Merge pull request #1113 from RumovZ/backend-columns

Backend columns
This commit is contained in:
Damien Elmes 2021-04-12 16:05:11 +10:00 committed by GitHub
commit 3f3c509bad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 696 additions and 794 deletions

View File

@ -7,8 +7,8 @@ browsing-all-fields = All Fields
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-flag = Any Flag
browsing-average-ease = Average Ease
browsing-average-interval = Average Interval
browsing-average-ease = Avg. Ease
browsing-average-interval = Avg. Interval
browsing-browser-appearance = Browser Appearance
browsing-browser-options = Browser Options
browsing-buried = Buried
@ -101,7 +101,7 @@ browsing-suspended = Suspended
browsing-tag-duplicates = Tag Duplicates
browsing-tag-rename-warning-empty = You can't rename a tag that has no notes.
browsing-target-field = Target field:
browsing-toggle-cards-notes-mode = Toggle Cards/Notes Mode
browsing-toggle-showing-cards-notes = Toggle Showing Cards/Notes
browsing-toggle-mark = Toggle Mark
browsing-toggle-suspend = Toggle Suspend
browsing-treat-input-as-regular-expression = Treat input as regular expression

View File

@ -4,6 +4,7 @@ persistent = no
[TYPECHECK]
ignored-classes=
BrowserColumns,
BrowserRow,
FormatTimespanIn,
AnswerCardIn,

View File

@ -12,7 +12,6 @@ SearchNode = _pb.SearchNode
Progress = _pb.Progress
EmptyCardsReport = _pb.EmptyCardsReport
GraphPreferences = _pb.GraphPreferences
BuiltinSort = _pb.SortOrder.Builtin
Preferences = _pb.Preferences
UndoStatus = _pb.UndoStatus
OpChanges = _pb.OpChanges
@ -21,6 +20,7 @@ OpChangesWithId = _pb.OpChangesWithId
OpChangesAfterUndo = _pb.OpChangesAfterUndo
DefaultsForAdding = _pb.DeckAndNotetype
BrowserRow = _pb.BrowserRow
BrowserColumns = _pb.BrowserColumns
import copy
import os
@ -40,7 +40,7 @@ from anki.config import Config, ConfigManager
from anki.consts import *
from anki.dbproxy import DBProxy
from anki.decks import Deck, DeckConfig, DeckConfigId, DeckId, DeckManager
from anki.errors import AbortSchemaModification, DBError
from anki.errors import AbortSchemaModification, DBError, InvalidInput
from anki.lang import FormatTimeSpan
from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager, Notetype, NotetypeDict, NotetypeId
@ -505,7 +505,7 @@ class Collection:
def find_cards(
self,
query: str,
order: Union[bool, str, BuiltinSort.Kind.V] = False,
order: Union[bool, str, BrowserColumns.Column] = False,
reverse: bool = False,
) -> Sequence[CardId]:
"""Return card ids matching the provided search.
@ -520,13 +520,15 @@ class Collection:
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)
If order is a BrowserColumns.Column that supports sorting, sort using that
column. All available columns are available through col.all_browser_columns()
or browser.table._model.columns and support sorting unless column.sorting
is set to BrowserColumns.SORTING_NONE.
The reverse argument only applies when a BuiltinSort.Kind is provided;
The reverse argument only applies when a BrowserColumns.Column is provided;
otherwise the collection config defines whether reverse is set or not.
"""
mode = _build_sort_mode(order, reverse)
mode = self._build_sort_mode(order, reverse, False)
return cast(
Sequence[CardId], self._backend.search_cards(search=query, order=mode)
)
@ -534,7 +536,7 @@ class Collection:
def find_notes(
self,
query: str,
order: Union[bool, str, BuiltinSort.Kind.V] = False,
order: Union[bool, str, BrowserColumns.Column] = False,
reverse: bool = False,
) -> Sequence[NoteId]:
"""Return note ids matching the provided search.
@ -542,11 +544,38 @@ class Collection:
To programmatically construct a search string, see .build_search_string().
The order parameter is documented in .find_cards().
"""
mode = _build_sort_mode(order, reverse)
mode = self._build_sort_mode(order, reverse, True)
return cast(
Sequence[NoteId], self._backend.search_notes(search=query, order=mode)
)
def _build_sort_mode(
self,
order: Union[bool, str, BrowserColumns.Column],
reverse: bool,
finding_notes: bool,
) -> _pb.SortOrder:
if isinstance(order, str):
return _pb.SortOrder(custom=order)
if isinstance(order, bool):
if order is False:
return _pb.SortOrder(none=_pb.Empty())
# order=True: set args to sort column and reverse from config
sort_key = "noteSortType" if finding_notes else "sortType"
order = self.get_browser_column(self.get_config(sort_key))
reverse_key = (
Config.Bool.BROWSER_NOTE_SORT_BACKWARDS
if finding_notes
else Config.Bool.BROWSER_SORT_BACKWARDS
)
reverse = self.get_config_bool(reverse_key)
if isinstance(order, BrowserColumns.Column):
if order.sorting != BrowserColumns.SORTING_NONE:
return _pb.SortOrder(
builtin=_pb.SortOrder.Builtin(column=order.key, reverse=reverse)
)
raise InvalidInput(f"{order} is not a valid sort order.")
def find_and_replace(
self,
*,
@ -696,6 +725,15 @@ class Collection:
# Browser Table
##########################################################################
def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
return self._backend.all_browser_columns()
def get_browser_column(self, key: str) -> Optional[BrowserColumns.Column]:
for column in self._backend.all_browser_columns():
if column.key == key:
return column
return None
def browser_row_for_id(
self, id_: int
) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
@ -712,24 +750,24 @@ class Collection:
columns = self.get_config(
"activeCols", ["noteFld", "template", "cardDue", "deck"]
)
self._backend.set_desktop_browser_card_columns(columns)
self._backend.set_active_browser_columns(columns)
return columns
def set_browser_card_columns(self, columns: List[str]) -> None:
self.set_config("activeCols", columns)
self._backend.set_desktop_browser_card_columns(columns)
self._backend.set_active_browser_columns(columns)
def load_browser_note_columns(self) -> List[str]:
"""Return the stored note column names and ensure the backend columns are set and in sync."""
columns = self.get_config(
"activeNoteCols", ["noteFld", "note", "noteCards", "noteTags"]
)
self._backend.set_desktop_browser_note_columns(columns)
self._backend.set_active_browser_columns(columns)
return columns
def set_browser_note_columns(self, columns: List[str]) -> None:
self.set_config("activeNoteCols", columns)
self._backend.set_desktop_browser_note_columns(columns)
self._backend.set_active_browser_columns(columns)
# Config
##########################################################################
@ -1111,18 +1149,3 @@ class _ReviewsUndo:
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
def _build_sort_mode(
order: Union[bool, str, BuiltinSort.Kind.V],
reverse: bool,
) -> _pb.SortOrder:
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))

View File

@ -1,7 +1,7 @@
# coding: utf-8
import pytest
from anki.collection import BuiltinSort, Config
from anki.collection import Config
from anki.consts import *
from tests.shared import getEmptyCol, isNearCutoff
@ -125,10 +125,12 @@ def test_findCards():
col.flush()
assert col.findCards("", order=True)[0] in latestCardIds
assert (
col.find_cards("", order=BuiltinSort.CARD_DUE, reverse=False)[0] == firstCardId
col.find_cards("", order=col.get_browser_column("cardDue"), reverse=False)[0]
== firstCardId
)
assert (
col.find_cards("", order=BuiltinSort.CARD_DUE, reverse=True)[0] != firstCardId
col.find_cards("", order=col.get_browser_column("cardDue"), reverse=True)[0]
!= firstCardId
)
# model
assert len(col.findCards("note:basic")) == 3

View File

@ -6,6 +6,7 @@ ignore = forms,hooks_gen.py
[TYPECHECK]
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
ignored-classes=
BrowserColumns,
BrowserRow,
SearchNode,
Config,

View File

@ -64,7 +64,6 @@ from aqt.utils import (
save_combo_history,
save_combo_index_for_session,
saveGeom,
saveHeader,
saveSplitter,
saveState,
shortcut,
@ -228,10 +227,10 @@ class Browser(QMainWindow):
def _closeWindow(self) -> None:
self._cleanup_preview()
self.editor.cleanup()
self.table.cleanup()
saveSplitter(self.form.splitter, "editor3")
saveGeom(self, "editor")
saveState(self, "editor")
saveHeader(self.form.tableView.horizontalHeader(), "editor")
self.teardownHooks()
self.mw.maybeReset()
aqt.dialogs.markClosed("Browser")
@ -383,7 +382,7 @@ class Browser(QMainWindow):
self.table.set_view(self.form.tableView)
switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial())
switch.setChecked(self.table.is_notes_mode())
switch.setToolTip(tr.browsing_toggle_cards_notes_mode())
switch.setToolTip(tr.browsing_toggle_showing_cards_notes())
qconnect(self.form.action_toggle_mode.triggered, switch.toggle)
qconnect(switch.toggled, self.on_table_state_changed)
self.form.gridLayout.addWidget(switch, 0, 0)

View File

@ -607,10 +607,10 @@
</action>
<action name="action_toggle_mode">
<property name="text">
<string>browsing_toggle_cards_notes_mode</string>
<string>browsing_toggle_showing_cards_notes</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+M</string>
<string notr="true">Alt+T</string>
</property>
</action>
</widget>

View File

@ -6,7 +6,6 @@ from __future__ import annotations
import time
from abc import ABC, abstractmethod, abstractproperty
from dataclasses import dataclass
from operator import itemgetter
from typing import (
Any,
Callable,
@ -23,6 +22,7 @@ from typing import (
import aqt
import aqt.forms
from anki.cards import Card, CardId
from anki.collection import BrowserColumns as Columns
from anki.collection import BrowserRow, Collection, Config, OpChanges
from anki.consts import *
from anki.errors import NotFoundError
@ -35,10 +35,12 @@ from aqt.utils import (
KeyboardModifiersPressed,
qtMenuShortcutWorkaround,
restoreHeader,
saveHeader,
showInfo,
tr,
)
Column = Columns.Column
ItemId = Union[CardId, NoteId]
ItemList = Union[Sequence[CardId], Sequence[NoteId]]
@ -47,7 +49,8 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]]
class SearchContext:
search: str
browser: aqt.browser.Browser
order: Union[bool, str] = True
order: Union[bool, str, Column] = True
reverse: bool = False
# if set, provided ids will be used instead of the regular search
ids: Optional[Sequence[ItemId]] = None
@ -73,6 +76,9 @@ class Table:
self._setup_view()
self._setup_headers()
def cleanup(self) -> None:
self._save_header()
# Public Methods
######################################################################
@ -148,11 +154,8 @@ class Table:
def select_single_card(self, card_id: CardId) -> None:
"""Try to set the selection to the item corresponding to the given card."""
self.clear_selection()
if self.is_notes_mode():
self._view.selectRow(0)
else:
if (row := self._model.get_card_row(card_id)) is not None:
self._view.selectRow(row)
if (row := self._model.get_card_row(card_id)) is not None:
self._view.selectRow(row)
# Reset
@ -198,6 +201,7 @@ class Table:
def toggle_state(self, is_notes_mode: bool, last_search: str) -> None:
if is_notes_mode == self.is_notes_mode():
return
self._save_header()
self._save_selection()
self._state = self._model.toggle_state(
SearchContext(search=last_search, browser=self.browser)
@ -205,8 +209,7 @@ class Table:
self.col.set_config_bool(
Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, self.is_notes_mode()
)
self._set_sort_indicator()
self._set_column_sizes()
self._restore_header()
self._restore_selection(self._toggled_selection)
# Move cursor
@ -273,6 +276,12 @@ class Table:
# this must be set post-resize or it doesn't work
hh.setCascadingSectionResizes(False)
def _save_header(self) -> None:
saveHeader(self._view.horizontalHeader(), self._state.config_key_prefix)
def _restore_header(self) -> None:
restoreHeader(self._view.horizontalHeader(), self._state.config_key_prefix)
# Setup
def _setup_view(self) -> None:
@ -314,14 +323,14 @@ class Table:
if not isWin:
vh.hide()
hh.show()
restoreHeader(hh, "editor")
hh.setHighlightSections(False)
hh.setMinimumSectionSize(50)
hh.setSectionsMovable(True)
self._set_column_sizes()
hh.setContextMenuPolicy(Qt.CustomContextMenu)
qconnect(hh.customContextMenuRequested, self._on_header_context)
self._restore_header()
self._set_column_sizes()
self._set_sort_indicator()
qconnect(hh.customContextMenuRequested, self._on_header_context)
qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed)
qconnect(hh.sectionMoved, self._on_column_moved)
@ -350,13 +359,13 @@ class Table:
def _on_header_context(self, pos: QPoint) -> None:
gpos = self._view.mapToGlobal(pos)
m = QMenu()
for column, name in self._state.columns:
a = m.addAction(name)
for key, column in self._model.columns.items():
a = m.addAction(self._state.column_label(column))
a.setCheckable(True)
a.setChecked(self._model.active_column_index(column) is not None)
a.setChecked(self._model.active_column_index(key) is not None)
qconnect(
a.toggled,
lambda checked, column=column: self._on_column_toggled(checked, column),
lambda checked, key=key: self._on_column_toggled(checked, key),
)
gui_hooks.browser_header_will_show_context_menu(self.browser, m)
m.exec_(gpos)
@ -375,16 +384,18 @@ class Table:
if checked:
self._scroll_to_column(self._model.len_columns() - 1)
def _on_sort_column_changed(self, index: int, order: int) -> None:
def _on_sort_column_changed(self, section: int, order: int) -> None:
order = bool(order)
sort_column = self._model.active_column(index)
if sort_column in ("question", "answer"):
column = self._model.column_at_section(section)
if column.sorting == Columns.SORTING_NONE:
showInfo(tr.browsing_sorting_on_this_column_is_not())
sort_column = self._state.sort_column
if self._state.sort_column != sort_column:
self._state.sort_column = sort_column
sort_key = self._state.sort_column
else:
sort_key = column.key
if self._state.sort_column != sort_key:
self._state.sort_column = sort_key
# default to descending for non-text fields
if sort_column == "noteFld":
if column.sorting == Columns.SORTING_REVERSED:
order = not order
self._state.sort_backwards = order
self.browser.search()
@ -523,7 +534,7 @@ class Table:
class ItemState(ABC):
_columns: List[Tuple[str, str]]
config_key_prefix: str
_active_columns: List[str]
_sort_column: str
_sort_backwards: bool
@ -545,14 +556,18 @@ class ItemState(ABC):
def card_ids_from_note_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
return self.col.db.list(f"select id from cards where nid in {ids2str(items)}")
def column_key_at(self, index: int) -> str:
return self._active_columns[index]
def column_label(self, column: Column) -> str:
return (
column.notes_mode_label if self.is_notes_mode() else column.cards_mode_label
)
# Columns and sorting
# abstractproperty is deprecated but used due to mypy limitations
# (https://github.com/python/mypy/issues/1362)
@abstractproperty
def columns(self) -> List[Tuple[str, str]]:
"""Return all for the state available columns."""
@abstractproperty
def active_columns(self) -> List[str]:
"""Return the saved or default columns for the state."""
@ -590,7 +605,9 @@ class ItemState(ABC):
# Get ids
@abstractmethod
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool
) -> Sequence[ItemId]:
"""Return the item ids fitting the given search and order."""
@abstractmethod
@ -619,37 +636,13 @@ class ItemState(ABC):
class CardState(ItemState):
def __init__(self, col: Collection) -> None:
super().__init__(col)
self._load_columns()
self.config_key_prefix = "editor"
self._active_columns = self.col.load_browser_card_columns()
self._sort_column = self.col.get_config("sortType")
self._sort_backwards = self.col.get_config_bool(
Config.Bool.BROWSER_SORT_BACKWARDS
)
def _load_columns(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))
@property
def columns(self) -> List[Tuple[str, str]]:
return self._columns
@property
def active_columns(self) -> List[str]:
return self._active_columns
@ -685,8 +678,10 @@ class CardState(ItemState):
def get_note(self, item: ItemId) -> Note:
return self.get_card(item).note()
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
return self.col.find_cards(search, order)
def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool
) -> Sequence[ItemId]:
return self.col.find_cards(search, order, reverse)
def get_item_from_card_id(self, card: CardId) -> ItemId:
return card
@ -707,33 +702,13 @@ class CardState(ItemState):
class NoteState(ItemState):
def __init__(self, col: Collection) -> None:
super().__init__(col)
self._load_columns()
self.config_key_prefix = "editorNotesMode"
self._active_columns = self.col.load_browser_note_columns()
self._sort_column = self.col.get_config("noteSortType")
self._sort_backwards = self.col.get_config_bool(
Config.Bool.BROWSER_NOTE_SORT_BACKWARDS
)
def _load_columns(self) -> None:
self._columns = [
("note", tr.browsing_note()),
("noteCards", tr.editing_cards()),
("noteCrt", tr.browsing_created()),
("noteDue", tr.statistics_due_date()),
("noteEase", tr.browsing_average_ease()),
("noteFld", tr.browsing_sort_field()),
("noteIvl", tr.browsing_average_interval()),
("noteLapses", tr.scheduling_lapses()),
("noteMod", tr.search_note_modified()),
("noteReps", tr.scheduling_reviews()),
("noteTags", tr.editing_tags()),
]
self._columns.sort(key=itemgetter(1))
@property
def columns(self) -> List[Tuple[str, str]]:
return self._columns
@property
def active_columns(self) -> List[str]:
return self._active_columns
@ -769,11 +744,13 @@ class NoteState(ItemState):
def get_note(self, item: ItemId) -> Note:
return self.col.get_note(NoteId(item))
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
return self.col.find_notes(search, order)
def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool
) -> Sequence[ItemId]:
return self.col.find_notes(search, order, reverse)
def get_item_from_card_id(self, card: CardId) -> ItemId:
return self.get_card(card).note().id
return self.col.get_card(card).note().id
def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
return super().card_ids_from_note_ids(items)
@ -854,13 +831,27 @@ def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str,
class DataModel(QAbstractTableModel):
"""Data manager for the browser table.
_items -- The card or note ids currently hold and corresponding to the
table's rows.
_rows -- The cached data objects to render items to rows.
columns -- The data objects of all available columns, used to define the display
of active columns and list all toggleable columns to the user.
_block_updates -- If True, serve stale content to avoid hitting the DB.
_stale_cutoff -- A threshold to decide whether a cached row has gone stale.
"""
def __init__(self, col: Collection, state: ItemState) -> None:
QAbstractTableModel.__init__(self)
self.col: Collection = col
self.columns: Dict[str, Column] = dict(
((c.key, c) for c in self.col.all_browser_columns())
)
gui_hooks.browser_did_fetch_columns(self.columns)
self._state: ItemState = state
self._items: Sequence[ItemId] = []
self._rows: Dict[int, CellRow] = {}
# serve stale content to avoid hitting the DB?
self._block_updates = False
self._stale_cutoff = 0.0
@ -1010,9 +1001,18 @@ class DataModel(QAbstractTableModel):
def search(self, context: SearchContext) -> None:
self.begin_reset()
try:
if context.order is True:
try:
context.order = self.columns[self._state.sort_column]
except KeyError:
# invalid sort column in config
context.order = self.columns["noteCrt"]
context.reverse = self._state.sort_backwards
gui_hooks.browser_will_search(context)
if context.ids is None:
context.ids = self._state.find_items(context.search, context.order)
context.ids = self._state.find_items(
context.search, context.order, context.reverse
)
gui_hooks.browser_did_search(context)
self._items = context.ids
self._rows = {}
@ -1026,8 +1026,19 @@ class DataModel(QAbstractTableModel):
# Columns
def active_column(self, index: int) -> str:
return self._state.active_columns[index]
def column_at(self, index: QModelIndex) -> Column:
return self.column_at_section(index.column())
def column_at_section(self, section: int) -> Column:
"""Returns the column object corresponding to the active column at index or the default
column object if no data is associated with the active column.
"""
key = self._state.column_key_at(section)
try:
return self.columns[key]
except KeyError:
self.columns[key] = addon_column_fillin(key)
return self.columns[key]
def active_column_index(self, column: str) -> Optional[int]:
return (
@ -1058,11 +1069,7 @@ class DataModel(QAbstractTableModel):
if not index.isValid():
return QVariant()
if role == Qt.FontRole:
if self.active_column(index.column()) not in (
"question",
"answer",
"noteFld",
):
if not self.column_at(index).uses_cell_font:
return QVariant()
qfont = QFont()
row = self.get_row(index)
@ -1071,15 +1078,7 @@ class DataModel(QAbstractTableModel):
return qfont
if role == Qt.TextAlignmentRole:
align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter
if self.active_column(index.column()) not in (
"question",
"answer",
"template",
"deck",
"noteFld",
"note",
"noteTags",
):
if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
align |= Qt.AlignHCenter
return align
if role in (Qt.DisplayRole, Qt.ToolTipRole):
@ -1089,21 +1088,9 @@ class DataModel(QAbstractTableModel):
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 < self.len_columns():
column = self.active_column(section)
txt = None
for stype, name in self._state.columns:
if column == 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
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._state.column_label(self.column_at_section(section))
return None
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if self.get_row(index).is_deleted:
@ -1131,3 +1118,17 @@ class StatusDelegate(QItemDelegate):
painter.fillRect(option.rect, brush)
painter.restore()
return QItemDelegate.paint(self, painter, option, index)
def addon_column_fillin(key: str) -> Column:
"""Return a column with generic fields and a label indicating to the user that this column was
added by an add-on.
"""
return Column(
key=key,
cards_mode_label=tr.browsing_addon(),
notes_mode_label=tr.browsing_addon(),
sorting=Columns.SORTING_NONE,
uses_cell_font=False,
alignment=Columns.ALIGNMENT_CENTER,
)

View File

@ -16,7 +16,7 @@ prefix = """\
from __future__ import annotations
from typing import Any, Callable, List, Sequence, Tuple, Optional, Union
from typing import Any, Callable, Dict, List, Sequence, Tuple, Optional, Union
import anki
import aqt
@ -422,6 +422,18 @@ hooks = [
represents.
""",
),
Hook(
name="browser_did_fetch_columns",
args=["columns: Dict[str, aqt.table.Column]"],
doc="""Allows you to add custom columns to the browser.
columns is a dictionary of data obejcts. You can add an entry with a custom
column to describe how it should be displayed in the browser or modify
existing entries.
Every column in the dictionary will be toggleable by the user.
""",
),
# Main window states
###################
# these refer to things like deckbrowser, overview and reviewer state,

View File

@ -250,9 +250,9 @@ service SearchService {
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount);
rpc AllBrowserColumns(Empty) returns (BrowserColumns);
rpc BrowserRowForId(Int64) returns (BrowserRow);
rpc SetDesktopBrowserCardColumns(StringList) returns (Empty);
rpc SetDesktopBrowserNoteColumns(StringList) returns (Empty);
rpc SetActiveBrowserColumns(StringList) returns (Empty);
}
service StatsService {
@ -797,35 +797,13 @@ message SearchOut {
message SortOrder {
message Builtin {
enum Kind {
NOTE_CARDS = 0;
NOTE_CREATION = 1;
NOTE_DUE = 2;
NOTE_EASE = 3;
NOTE_FIELD = 4;
NOTE_INTERVAL = 5;
NOTE_LAPSES = 6;
NOTE_MOD = 7;
NOTE_REPS = 8;
NOTE_TAGS = 9;
NOTETYPE = 10;
CARD_MOD = 11;
CARD_REPS = 12;
CARD_DUE = 13;
CARD_EASE = 14;
CARD_LAPSES = 15;
CARD_INTERVAL = 16;
CARD_DECK = 17;
CARD_TEMPLATE = 18;
}
Kind kind = 1;
string column = 1;
bool reverse = 2;
}
oneof value {
Empty from_config = 1;
Empty none = 2;
string custom = 3;
Builtin builtin = 4;
Empty none = 1;
string custom = 2;
Builtin builtin = 3;
}
}
@ -1054,6 +1032,27 @@ message FindAndReplaceIn {
string field_name = 6;
}
message BrowserColumns {
enum Sorting {
SORTING_NONE = 0;
SORTING_NORMAL = 1;
SORTING_REVERSED = 2;
}
enum Alignment {
ALIGNMENT_START = 0;
ALIGNMENT_CENTER = 1;
}
message Column {
string key = 1;
string cards_mode_label = 2;
string notes_mode_label = 3;
Sorting sorting = 4;
bool uses_cell_font = 5;
Alignment alignment = 6;
}
repeated Column columns = 1;
}
message BrowserRow {
message Cell {
string text = 1;

View File

@ -1,73 +1,29 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{backend_proto as pb, browser_table};
use std::str::FromStr;
use crate::{backend_proto as pb, browser_table, i18n::I18n};
impl browser_table::Column {
pub fn to_pb_column(self, i18n: &I18n) -> pb::browser_columns::Column {
pb::browser_columns::Column {
key: self.to_string(),
cards_mode_label: self.cards_mode_label(i18n),
notes_mode_label: self.notes_mode_label(i18n),
sorting: self.sorting() as i32,
uses_cell_font: self.uses_cell_font(),
alignment: self.alignment() as i32,
}
}
}
impl From<pb::StringList> for Vec<browser_table::Column> {
fn from(input: pb::StringList) -> Self {
input.vals.into_iter().map(Into::into).collect()
}
}
impl From<String> for browser_table::Column {
fn from(text: String) -> Self {
match text.as_str() {
"question" => browser_table::Column::Question,
"answer" => browser_table::Column::Answer,
"deck" => browser_table::Column::CardDeck,
"cardDue" => browser_table::Column::CardDue,
"cardEase" => browser_table::Column::CardEase,
"cardLapses" => browser_table::Column::CardLapses,
"cardIvl" => browser_table::Column::CardInterval,
"cardMod" => browser_table::Column::CardMod,
"cardReps" => browser_table::Column::CardReps,
"template" => browser_table::Column::CardTemplate,
"noteCards" => browser_table::Column::NoteCards,
"noteCrt" => browser_table::Column::NoteCreation,
"noteDue" => browser_table::Column::NoteDue,
"noteEase" => browser_table::Column::NoteEase,
"noteFld" => browser_table::Column::NoteField,
"noteIvl" => browser_table::Column::NoteInterval,
"noteLapses" => browser_table::Column::NoteLapses,
"noteMod" => browser_table::Column::NoteMod,
"noteReps" => browser_table::Column::NoteReps,
"noteTags" => browser_table::Column::NoteTags,
"note" => browser_table::Column::Notetype,
_ => browser_table::Column::Custom,
}
}
}
impl From<browser_table::Row> for pb::BrowserRow {
fn from(row: browser_table::Row) -> Self {
pb::BrowserRow {
cells: row.cells.into_iter().map(Into::into).collect(),
color: row.color.into(),
font_name: row.font.name,
font_size: row.font.size,
}
}
}
impl From<browser_table::Cell> for pb::browser_row::Cell {
fn from(cell: browser_table::Cell) -> Self {
pb::browser_row::Cell {
text: cell.text,
is_rtl: cell.is_rtl,
}
}
}
impl From<browser_table::Color> for i32 {
fn from(color: browser_table::Color) -> Self {
match color {
browser_table::Color::Default => pb::browser_row::Color::Default as i32,
browser_table::Color::Marked => pb::browser_row::Color::Marked as i32,
browser_table::Color::Suspended => pb::browser_row::Color::Suspended as i32,
browser_table::Color::FlagRed => pb::browser_row::Color::FlagRed as i32,
browser_table::Color::FlagOrange => pb::browser_row::Color::FlagOrange as i32,
browser_table::Color::FlagGreen => pb::browser_row::Color::FlagGreen as i32,
browser_table::Color::FlagBlue => pb::browser_row::Color::FlagBlue as i32,
}
input
.vals
.iter()
.map(|c| browser_table::Column::from_str(c).unwrap_or_default())
.collect()
}
}

View File

@ -4,15 +4,13 @@
mod browser_table;
mod search_node;
use std::convert::TryInto;
use std::{convert::TryInto, str::FromStr, sync::Arc};
use super::Backend;
use crate::{
backend_proto as pb,
backend_proto::{
sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto,
},
config::SortKind,
backend_proto::sort_order::Value as SortOrderProto,
browser_table::Column,
prelude::*,
search::{concatenate_searches, replace_search_node, write_nodes, Node, SortMode},
};
@ -89,56 +87,31 @@ impl SearchService for Backend {
})
}
fn all_browser_columns(&self, _input: pb::Empty) -> Result<pb::BrowserColumns> {
self.with_col(|col| Ok(col.all_browser_columns()))
}
fn set_active_browser_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
self.with_col(|col| {
col.state.active_browser_columns = Some(Arc::new(input.into()));
Ok(())
})
.map(Into::into)
}
fn browser_row_for_id(&self, input: pb::Int64) -> Result<pb::BrowserRow> {
self.with_col(|col| col.browser_row_for_id(input.val).map(Into::into))
}
fn set_desktop_browser_card_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
self.with_col(|col| col.set_desktop_browser_card_columns(input.into()))?;
Ok(().into())
}
fn set_desktop_browser_note_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
self.with_col(|col| col.set_desktop_browser_note_columns(input.into()))?;
Ok(().into())
}
}
impl From<SortKindProto> for SortKind {
fn from(kind: SortKindProto) -> Self {
match kind {
SortKindProto::NoteCards => SortKind::NoteCards,
SortKindProto::NoteCreation => SortKind::NoteCreation,
SortKindProto::NoteDue => SortKind::NoteDue,
SortKindProto::NoteEase => SortKind::NoteEase,
SortKindProto::NoteInterval => SortKind::NoteInterval,
SortKindProto::NoteLapses => SortKind::NoteLapses,
SortKindProto::NoteMod => SortKind::NoteMod,
SortKindProto::NoteField => SortKind::NoteField,
SortKindProto::NoteReps => SortKind::NoteReps,
SortKindProto::NoteTags => SortKind::NoteTags,
SortKindProto::Notetype => SortKind::Notetype,
SortKindProto::CardMod => SortKind::CardMod,
SortKindProto::CardReps => SortKind::CardReps,
SortKindProto::CardDue => SortKind::CardDue,
SortKindProto::CardEase => SortKind::CardEase,
SortKindProto::CardLapses => SortKind::CardLapses,
SortKindProto::CardInterval => SortKind::CardInterval,
SortKindProto::CardDeck => SortKind::CardDeck,
SortKindProto::CardTemplate => SortKind::CardTemplate,
}
}
}
impl From<Option<SortOrderProto>> for SortMode {
fn from(order: Option<SortOrderProto>) -> Self {
use pb::sort_order::Value as V;
match order.unwrap_or(V::FromConfig(pb::Empty {})) {
match order.unwrap_or(V::None(pb::Empty {})) {
V::None(_) => SortMode::NoOrder,
V::Custom(s) => SortMode::Custom(s),
V::FromConfig(_) => SortMode::FromConfig,
V::Builtin(b) => SortMode::Builtin {
kind: b.kind().into(),
column: Column::from_str(&b.column).unwrap_or_default(),
reverse: b.reverse,
},
}

View File

@ -4,11 +4,12 @@
use std::sync::Arc;
use itertools::Itertools;
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use crate::error::{AnkiError, Result};
use crate::i18n::I18n;
use crate::{
backend_proto as pb,
card::{Card, CardId, CardQueue, CardType},
collection::Collection,
config::BoolKey,
@ -21,112 +22,47 @@ use crate::{
timestamp::{TimestampMillis, TimestampSecs},
};
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone, Copy)]
#[repr(u8)]
#[derive(Debug, PartialEq, Clone, Copy, Display, EnumIter, EnumString)]
#[strum(serialize_all = "camelCase")]
pub enum Column {
#[strum(serialize = "")]
Custom,
Question,
Answer,
CardDeck,
CardDue,
CardEase,
CardLapses,
CardInterval,
CardMod,
CardReps,
CardTemplate,
NoteCards,
#[strum(serialize = "template")]
Cards,
Deck,
#[strum(serialize = "cardDue")]
Due,
#[strum(serialize = "cardEase")]
Ease,
#[strum(serialize = "cardLapses")]
Lapses,
#[strum(serialize = "cardIvl")]
Interval,
#[strum(serialize = "noteCrt")]
NoteCreation,
NoteDue,
NoteEase,
NoteField,
NoteInterval,
NoteLapses,
NoteMod,
NoteReps,
NoteTags,
#[strum(serialize = "note")]
Notetype,
Question,
#[strum(serialize = "cardReps")]
Reps,
#[strum(serialize = "noteFld")]
SortField,
#[strum(serialize = "noteTags")]
Tags,
}
#[derive(Debug, PartialEq)]
pub struct Row {
pub cells: Vec<Cell>,
pub color: Color,
pub font: Font,
}
#[derive(Debug, PartialEq)]
pub struct Cell {
pub text: String,
pub is_rtl: bool,
}
#[derive(Debug, PartialEq)]
pub enum Color {
Default,
Marked,
Suspended,
FlagRed,
FlagOrange,
FlagGreen,
FlagBlue,
}
#[derive(Debug, PartialEq)]
pub struct Font {
pub name: String,
pub size: u32,
}
trait RowContext {
fn get_cell_text(&mut self, column: Column) -> 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: Column) -> 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: Column) -> bool {
match column {
Column::NoteField => {
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: &[Column]) -> 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()?,
})
impl Default for Column {
fn default() -> Self {
Column::Custom
}
}
struct CardRowContext {
card: Card,
struct RowContext {
notes_mode: bool,
cards: Vec<Card>,
note: Note,
notetype: Arc<Notetype>,
deck: Arc<Deck>,
@ -143,14 +79,6 @@ struct RenderContext {
answer_nodes: Vec<RenderedNode>,
}
struct NoteRowContext {
note: Note,
notetype: Arc<Notetype>,
cards: Vec<Card>,
tr: I18n,
timing: SchedTimingToday,
}
fn card_render_required(columns: &[Column]) -> bool {
columns
.iter()
@ -200,20 +128,88 @@ impl Note {
}
}
impl Collection {
pub fn browser_row_for_id(&mut self, id: i64) -> Result<Row> {
if self.get_bool(BoolKey::BrowserTableShowNotesMode) {
let columns = self
.get_desktop_browser_note_columns()
.ok_or_else(|| AnkiError::invalid_input("Note columns not set."))?;
NoteRowContext::new(self, id)?.browser_row_for_id(&columns)
} else {
let columns = self
.get_desktop_browser_card_columns()
.ok_or_else(|| AnkiError::invalid_input("Card columns not set."))?;
CardRowContext::new(self, id, card_render_required(&columns))?
.browser_row_for_id(&columns)
impl Column {
pub fn cards_mode_label(self, i18n: &I18n) -> String {
match self {
Self::Answer => i18n.browsing_answer(),
Self::CardMod => i18n.search_card_modified(),
Self::Cards => i18n.browsing_card(),
Self::Deck => i18n.decks_deck(),
Self::Due => i18n.statistics_due_date(),
Self::Custom => i18n.browsing_addon(),
Self::Ease => i18n.browsing_ease(),
Self::Interval => i18n.browsing_interval(),
Self::Lapses => i18n.scheduling_lapses(),
Self::NoteCreation => i18n.browsing_created(),
Self::NoteMod => i18n.search_note_modified(),
Self::Notetype => i18n.browsing_note(),
Self::Question => i18n.browsing_question(),
Self::Reps => i18n.scheduling_reviews(),
Self::SortField => i18n.browsing_sort_field(),
Self::Tags => i18n.editing_tags(),
}
.into()
}
pub fn notes_mode_label(self, i18n: &I18n) -> String {
match self {
Self::CardMod => i18n.search_card_modified(),
Self::Cards => i18n.editing_cards(),
Self::Ease => i18n.browsing_average_ease(),
Self::Interval => i18n.browsing_average_interval(),
Self::Reps => i18n.scheduling_reviews(),
_ => return self.cards_mode_label(i18n),
}
.into()
}
pub fn sorting(self) -> pb::browser_columns::Sorting {
use pb::browser_columns::Sorting;
match self {
Self::Question | Self::Answer | Self::Custom => Sorting::None,
Self::SortField => Sorting::Reversed,
_ => Sorting::Normal,
}
}
pub fn uses_cell_font(self) -> bool {
matches!(self, Self::Question | Self::Answer | Self::SortField)
}
pub fn alignment(self) -> pb::browser_columns::Alignment {
use pb::browser_columns::Alignment;
match self {
Self::Question
| Self::Answer
| Self::Cards
| Self::Deck
| Self::SortField
| Self::Notetype
| Self::Tags => Alignment::Start,
_ => Alignment::Center,
}
}
}
impl Collection {
pub fn all_browser_columns(&self) -> pb::BrowserColumns {
let mut columns: Vec<pb::browser_columns::Column> = Column::iter()
.filter(|&c| c != Column::Custom)
.map(|c| c.to_pb_column(&self.tr))
.collect();
columns.sort_by(|c1, c2| c1.cards_mode_label.cmp(&c2.cards_mode_label));
pb::BrowserColumns { columns }
}
pub fn browser_row_for_id(&mut self, id: i64) -> Result<pb::BrowserRow> {
let notes_mode = self.get_bool(BoolKey::BrowserTableShowNotesMode);
let columns = Arc::clone(
self.state
.active_browser_columns
.as_ref()
.ok_or_else(|| AnkiError::invalid_input("Active browser columns not set."))?,
);
RowContext::new(self, id, notes_mode, card_render_required(&columns))?.browser_row(&columns)
}
fn get_note_maybe_with_fields(&self, id: NoteId, _with_fields: bool) -> Result<Note> {
@ -259,20 +255,32 @@ impl RenderContext {
}
}
impl CardRowContext {
fn new(col: &mut Collection, id: i64, with_card_render: bool) -> Result<Self> {
let card = col
.storage
.get_card(CardId(id))?
.ok_or(AnkiError::NotFound)?;
let note = col.get_note_maybe_with_fields(card.note_id, with_card_render)?;
impl RowContext {
fn new(
col: &mut Collection,
id: i64,
notes_mode: bool,
with_card_render: bool,
) -> Result<Self> {
let cards;
let note;
if notes_mode {
note = col.get_note_maybe_with_fields(NoteId(id), with_card_render)?;
cards = col.storage.all_cards_of_note(note.id)?;
} else {
cards = vec![col
.storage
.get_card(CardId(id))?
.ok_or(AnkiError::NotFound)?];
note = col.get_note_maybe_with_fields(cards[0].note_id, with_card_render)?;
}
let notetype = col
.get_notetype(note.notetype_id)?
.ok_or(AnkiError::NotFound)?;
let deck = col.get_deck(card.deck_id)?.ok_or(AnkiError::NotFound)?;
let original_deck = if card.original_deck_id.0 != 0 {
let deck = col.get_deck(cards[0].deck_id)?.ok_or(AnkiError::NotFound)?;
let original_deck = if cards[0].original_deck_id.0 != 0 {
Some(
col.get_deck(card.original_deck_id)?
col.get_deck(cards[0].original_deck_id)?
.ok_or(AnkiError::NotFound)?,
)
} else {
@ -280,13 +288,14 @@ impl CardRowContext {
};
let timing = col.timing_today()?;
let render_context = if with_card_render {
Some(RenderContext::new(col, &card, &note, &notetype)?)
Some(RenderContext::new(col, &cards[0], &note, &notetype)?)
} else {
None
};
Ok(CardRowContext {
card,
Ok(RowContext {
notes_mode,
cards,
note,
notetype,
deck,
@ -297,8 +306,67 @@ impl CardRowContext {
})
}
fn browser_row(&self, columns: &[Column]) -> Result<pb::BrowserRow> {
Ok(pb::BrowserRow {
cells: columns
.iter()
.map(|&column| self.get_cell(column))
.collect::<Result<_>>()?,
color: self.get_row_color() as i32,
font_name: self.get_row_font_name()?,
font_size: self.get_row_font_size()?,
})
}
fn get_cell(&self, column: Column) -> Result<pb::browser_row::Cell> {
Ok(pb::browser_row::Cell {
text: self.get_cell_text(column)?,
is_rtl: self.get_is_rtl(column),
})
}
fn get_cell_text(&self, column: Column) -> Result<String> {
Ok(match column {
Column::Question => self.question_str(),
Column::Answer => self.answer_str(),
Column::Deck => self.deck_str(),
Column::Due => self.due_str(),
Column::Ease => self.ease_str(),
Column::Interval => self.interval_str(),
Column::Lapses => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
Column::CardMod => self.card_mod_str(),
Column::Reps => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
Column::Cards => self.cards_str()?,
Column::NoteCreation => self.note_creation_str(),
Column::SortField => self.note_field_str(),
Column::NoteMod => self.note.mtime.date_string(),
Column::Tags => self.note.tags.join(" "),
Column::Notetype => self.notetype.name.to_owned(),
Column::Custom => "".to_string(),
})
}
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: Column) -> bool {
match column {
Column::SortField => {
let index = self.notetype.config.sort_field_idx as usize;
self.notetype.fields[index].config.rtl
}
_ => false,
}
}
fn template(&self) -> Result<&CardTemplate> {
self.notetype.get_template(self.card.template_idx)
self.notetype.get_template(self.cards[0].template_idx)
}
fn answer_str(&self) -> String {
@ -326,149 +394,31 @@ impl CardRowContext {
.to_string()
}
fn card_due_str(&mut self) -> String {
let due = if self.card.is_filtered_deck() {
fn due_str(&self) -> String {
if self.notes_mode {
self.note_due_str()
} else {
self.card_due_str()
}
}
fn card_due_str(&self) -> String {
let due = if self.cards[0].is_filtered_deck() {
self.tr.browsing_filtered()
} else if self.card.is_new_type_or_queue() {
self.tr.statistics_due_for_new_card(self.card.due)
} else if let Some(time) = self.card.due_time(&self.timing) {
} else if self.cards[0].is_new_type_or_queue() {
self.tr.statistics_due_for_new_card(self.cards[0].due)
} else if let Some(time) = self.cards[0].due_time(&self.timing) {
time.date_string().into()
} else {
return "".into();
};
if self.card.is_undue_queue() {
if self.cards[0].is_undue_queue() {
format!("({})", due)
} else {
due.into()
}
}
fn card_ease_str(&self) -> String {
match self.card.ctype {
CardType::New => self.tr.browsing_new().into(),
_ => format!("{}%", self.card.ease_factor / 10),
}
}
fn card_interval_str(&self) -> String {
match self.card.ctype {
CardType::New => self.tr.browsing_new().into(),
CardType::Learn => self.tr.browsing_learning().into(),
_ => time_span((self.card.interval * 86400) as f32, &self.tr, false),
}
}
fn deck_str(&mut self) -> String {
let deck_name = self.deck.human_name();
if let Some(original_deck) = &self.original_deck {
format!("{} ({})", &deck_name, &original_deck.human_name())
} else {
deck_name
}
}
fn template_str(&self) -> Result<String> {
let name = &self.template()?.name;
Ok(match self.notetype.config.kind() {
NotetypeKind::Normal => name.to_owned(),
NotetypeKind::Cloze => format!("{} {}", name, self.card.template_idx + 1),
})
}
fn question_str(&self) -> String {
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
}
}
impl RowContext for CardRowContext {
fn get_cell_text(&mut self, column: Column) -> Result<String> {
Ok(match column {
Column::Question => self.question_str(),
Column::Answer => self.answer_str(),
Column::CardDeck => self.deck_str(),
Column::CardDue => self.card_due_str(),
Column::CardEase => self.card_ease_str(),
Column::CardInterval => self.card_interval_str(),
Column::CardLapses => self.card.lapses.to_string(),
Column::CardMod => self.card.mtime.date_string(),
Column::CardReps => self.card.reps.to_string(),
Column::CardTemplate => self.template_str()?,
Column::NoteCreation => self.note_creation_str(),
Column::NoteField => self.note_field_str(),
Column::NoteMod => self.note.mtime.date_string(),
Column::NoteTags => self.note.tags.join(" "),
Column::Notetype => self.notetype.name.to_owned(),
_ => "".to_string(),
})
}
fn get_row_color(&self) -> Color {
match self.card.flags {
1 => Color::FlagRed,
2 => Color::FlagOrange,
3 => Color::FlagGreen,
4 => Color::FlagBlue,
_ => {
if self.note.is_marked() {
Color::Marked
} else if self.card.queue == CardQueue::Suspended {
Color::Suspended
} else {
Color::Default
}
}
}
}
fn get_row_font(&self) -> Result<Font> {
Ok(Font {
name: self.template()?.config.browser_font_name.to_owned(),
size: self.template()?.config.browser_font_size,
})
}
fn note(&self) -> &Note {
&self.note
}
fn notetype(&self) -> &Notetype {
&self.notetype
}
}
impl NoteRowContext {
fn new(col: &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)?;
let timing = col.timing_today()?;
Ok(NoteRowContext {
note,
notetype,
cards,
tr: col.tr.clone(),
timing,
})
}
/// Returns the average ease of the non-new cards or a hint if there aren't any.
fn note_ease_str(&self) -> String {
let eases: Vec<u16> = self
.cards
.iter()
.filter(|c| c.ctype != CardType::New)
.map(|c| c.ease_factor)
.collect();
if eases.is_empty() {
self.tr.browsing_new().into()
} else {
format!("{}%", eases.iter().sum::<u16>() / eases.len() as u16 / 10)
}
}
/// Returns the due date of the next due card that is not in a filtered deck, new, suspended or
/// buried or the empty string if there is no such card.
fn note_due_str(&self) -> String {
@ -481,9 +431,30 @@ impl NoteRowContext {
.unwrap_or_else(|| "".into())
}
/// Returns the average interval of the review and relearn cards or the empty string if there
/// aren't any.
fn note_interval_str(&self) -> String {
/// Returns the average ease of the non-new cards or a hint if there aren't any.
fn ease_str(&self) -> String {
let eases: Vec<u16> = self
.cards
.iter()
.filter(|c| c.ctype != CardType::New)
.map(|c| c.ease_factor)
.collect();
if eases.is_empty() {
self.tr.browsing_new().into()
} else {
format!("{}%", eases.iter().sum::<u16>() / eases.len() as u16 / 10)
}
}
/// Returns the average interval of the review and relearn cards if there are any.
fn interval_str(&self) -> String {
if !self.notes_mode {
match self.cards[0].ctype {
CardType::New => return self.tr.browsing_new().into(),
CardType::Learn => return self.tr.browsing_learning().into(),
CardType::Review | CardType::Relearn => (),
}
}
let intervals: Vec<u32> = self
.cards
.iter()
@ -500,46 +471,79 @@ impl NoteRowContext {
)
}
}
}
impl RowContext for NoteRowContext {
fn get_cell_text(&mut self, column: Column) -> Result<String> {
Ok(match column {
Column::NoteCards => self.cards.len().to_string(),
Column::NoteCreation => self.note_creation_str(),
Column::NoteDue => self.note_due_str(),
Column::NoteEase => self.note_ease_str(),
Column::NoteField => self.note_field_str(),
Column::NoteInterval => self.note_interval_str(),
Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
Column::NoteMod => self.note.mtime.date_string(),
Column::NoteReps => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
Column::NoteTags => self.note.tags.join(" "),
Column::Notetype => self.notetype.name.to_owned(),
_ => "".to_string(),
})
fn card_mod_str(&self) -> String {
self.cards
.iter()
.map(|c| c.mtime)
.max()
.unwrap()
.date_string()
}
fn get_row_color(&self) -> Color {
if self.note.is_marked() {
Color::Marked
fn deck_str(&self) -> String {
if self.notes_mode {
let decks = self.cards.iter().map(|c| c.deck_id).unique().count();
if decks > 1 {
return format!("({})", decks);
}
}
let deck_name = self.deck.human_name();
if let Some(original_deck) = &self.original_deck {
format!("{} ({})", &deck_name, &original_deck.human_name())
} else {
Color::Default
deck_name
}
}
fn get_row_font(&self) -> Result<Font> {
Ok(Font {
name: "".to_owned(),
size: 0,
fn cards_str(&self) -> Result<String> {
Ok(if self.notes_mode {
self.cards.len().to_string()
} else {
let name = &self.template()?.name;
match self.notetype.config.kind() {
NotetypeKind::Normal => name.to_owned(),
NotetypeKind::Cloze => format!("{} {}", name, self.cards[0].template_idx + 1),
}
})
}
fn note(&self) -> &Note {
&self.note
fn question_str(&self) -> String {
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
}
fn notetype(&self) -> &Notetype {
&self.notetype
fn get_row_font_name(&self) -> Result<String> {
Ok(self.template()?.config.browser_font_name.to_owned())
}
fn get_row_font_size(&self) -> Result<u32> {
Ok(self.template()?.config.browser_font_size)
}
fn get_row_color(&self) -> pb::browser_row::Color {
use pb::browser_row::Color;
if self.notes_mode {
if self.note.is_marked() {
Color::Marked
} else {
Color::Default
}
} else {
match self.cards[0].flags {
1 => Color::FlagRed,
2 => Color::FlagOrange,
3 => Color::FlagGreen,
4 => Color::FlagBlue,
_ => {
if self.note.is_marked() {
Color::Marked
} else if self.cards[0].queue == CardQueue::Suspended {
Color::Suspended
} else {
Color::Default
}
}
}
}
}
}

View File

@ -3,6 +3,7 @@
use crate::types::Usn;
use crate::{
browser_table,
decks::{Deck, DeckId},
notetype::{Notetype, NotetypeId},
prelude::*,
@ -66,6 +67,7 @@ pub struct CollectionState {
pub(crate) deck_cache: HashMap<DeckId, Arc<Deck>>,
pub(crate) scheduler_info: Option<SchedulerInfo>,
pub(crate) card_queues: Option<CardQueues>,
pub(crate) active_browser_columns: Option<Arc<Vec<browser_table::Column>>>,
/// True if legacy Python code has executed SQL that has modified the
/// database, requiring modification time to be bumped.
pub(crate) modified_by_dbproxy: bool,

View File

@ -9,10 +9,8 @@ mod string;
pub(crate) mod undo;
pub use self::{bool::BoolKey, string::StringKey};
use crate::browser_table;
use crate::prelude::*;
use serde::{de::DeserializeOwned, Serialize};
use serde_derive::Deserialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use slog::warn;
use strum::IntoStaticStr;
@ -47,10 +45,6 @@ pub(crate) enum ConfigKey {
#[strum(to_string = "timeLim")]
AnswerTimeLimitSecs,
#[strum(to_string = "sortType")]
BrowserSortKind,
#[strum(to_string = "noteSortType")]
BrowserNoteSortKind,
#[strum(to_string = "curDeck")]
CurrentDeckId,
#[strum(to_string = "curModel")]
@ -65,9 +59,6 @@ pub(crate) enum ConfigKey {
NextNewCardPosition,
#[strum(to_string = "schedVer")]
SchedulerVersion,
DesktopBrowserCardColumns,
DesktopBrowserNoteColumns,
}
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
@ -132,38 +123,6 @@ impl Collection {
Ok(())
}
pub(crate) fn get_browser_sort_kind(&self) -> SortKind {
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) -> Option<Vec<browser_table::Column>> {
self.get_config_optional(ConfigKey::DesktopBrowserCardColumns)
}
pub(crate) fn set_desktop_browser_card_columns(
&mut self,
columns: Vec<browser_table::Column>,
) -> Result<()> {
self.set_config(ConfigKey::DesktopBrowserCardColumns, &columns)
.map(|_| ())
}
pub(crate) fn get_desktop_browser_note_columns(&self) -> Option<Vec<browser_table::Column>> {
self.get_config_optional(ConfigKey::DesktopBrowserNoteColumns)
}
pub(crate) fn set_desktop_browser_note_columns(
&mut self,
columns: Vec<browser_table::Column>,
) -> Result<()> {
self.set_config(ConfigKey::DesktopBrowserNoteColumns, &columns)
.map(|_| ())
}
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
self.get_config_optional(ConfigKey::CreationOffset)
}
@ -280,43 +239,6 @@ impl Collection {
}
}
#[derive(Deserialize, PartialEq, Debug, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum SortKind {
NoteCards,
#[serde(rename = "noteCrt")]
NoteCreation,
NoteDue,
NoteEase,
#[serde(rename = "noteIvl")]
NoteInterval,
NoteLapses,
NoteMod,
#[serde(rename = "noteFld")]
NoteField,
NoteReps,
#[serde(rename = "note")]
Notetype,
NoteTags,
CardMod,
CardReps,
CardDue,
CardEase,
CardLapses,
#[serde(rename = "cardIvl")]
CardInterval,
#[serde(rename = "deck")]
CardDeck,
#[serde(rename = "template")]
CardTemplate,
}
impl Default for SortKind {
fn default() -> Self {
Self::NoteCreation
}
}
// 2021 scheduler moves this into deck config
pub(crate) enum NewReviewMix {
Mix = 0,
@ -341,7 +263,6 @@ pub(crate) enum Weekday {
#[cfg(test)]
mod test {
use super::SortKind;
use crate::collection::open_test_collection;
use crate::decks::DeckId;
@ -349,7 +270,6 @@ mod test {
fn defaults() {
let col = open_test_collection();
assert_eq!(col.get_current_deck_id(), DeckId(1));
assert_eq!(col.get_browser_sort_kind(), SortKind::NoteField);
}
#[test]

View 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 MAX(mod);

View File

@ -14,19 +14,13 @@ use rusqlite::types::FromSql;
use std::borrow::Cow;
use crate::{
card::CardId,
card::CardType,
collection::Collection,
config::{BoolKey, SortKind},
error::Result,
notes::NoteId,
prelude::AnkiError,
search::parser::parse,
browser_table::Column, card::CardId, card::CardType, collection::Collection, error::Result,
notes::NoteId, prelude::AnkiError, search::parser::parse,
};
use sqlwriter::{RequiredTable, SqlWriter};
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SearchItems {
pub enum ReturnItemType {
Cards,
Notes,
}
@ -34,32 +28,31 @@ pub enum SearchItems {
#[derive(Debug, PartialEq, Clone)]
pub enum SortMode {
NoOrder,
FromConfig,
Builtin { kind: SortKind, reverse: bool },
Builtin { column: Column, reverse: bool },
Custom(String),
}
pub trait AsSearchItems {
fn as_search_items() -> SearchItems;
pub trait AsReturnItemType {
fn as_return_item_type() -> ReturnItemType;
}
impl AsSearchItems for CardId {
fn as_search_items() -> SearchItems {
SearchItems::Cards
impl AsReturnItemType for CardId {
fn as_return_item_type() -> ReturnItemType {
ReturnItemType::Cards
}
}
impl AsSearchItems for NoteId {
fn as_search_items() -> SearchItems {
SearchItems::Notes
impl AsReturnItemType for NoteId {
fn as_return_item_type() -> ReturnItemType {
ReturnItemType::Notes
}
}
impl SearchItems {
impl ReturnItemType {
fn required_table(&self) -> RequiredTable {
match self {
SearchItems::Cards => RequiredTable::Cards,
SearchItems::Notes => RequiredTable::Notes,
ReturnItemType::Cards => RequiredTable::Cards,
ReturnItemType::Notes => RequiredTable::Notes,
}
}
}
@ -68,8 +61,7 @@ impl SortMode {
fn required_table(&self) -> RequiredTable {
match self {
SortMode::NoOrder => RequiredTable::CardsOrNotes,
SortMode::FromConfig => unreachable!(),
SortMode::Builtin { kind, .. } => kind.required_table(),
SortMode::Builtin { column, .. } => column.required_table(),
SortMode::Custom(ref text) => {
if text.contains("n.") {
if text.contains("c.") {
@ -85,44 +77,31 @@ impl SortMode {
}
}
impl SortKind {
impl Column {
fn required_table(self) -> RequiredTable {
match self {
SortKind::NoteCards
| SortKind::NoteCreation
| SortKind::NoteDue
| SortKind::NoteEase
| SortKind::NoteField
| SortKind::NoteInterval
| SortKind::NoteLapses
| SortKind::NoteMod
| SortKind::NoteReps
| SortKind::NoteTags
| SortKind::Notetype => RequiredTable::Notes,
SortKind::CardTemplate => RequiredTable::CardsAndNotes,
SortKind::CardMod
| SortKind::CardReps
| SortKind::CardDue
| SortKind::CardEase
| SortKind::CardLapses
| SortKind::CardInterval
| SortKind::CardDeck => RequiredTable::Cards,
Column::Cards
| Column::NoteCreation
| Column::NoteMod
| Column::Notetype
| Column::SortField
| Column::Tags => RequiredTable::Notes,
_ => RequiredTable::CardsOrNotes,
}
}
}
impl Collection {
pub fn search<T>(&mut self, search: &str, mut mode: SortMode) -> Result<Vec<T>>
pub fn search<T>(&mut self, search: &str, mode: SortMode) -> Result<Vec<T>>
where
T: FromSql + AsSearchItems,
T: FromSql + AsReturnItemType,
{
let items = T::as_search_items();
let item_type = T::as_return_item_type();
let top_node = Node::Group(parse(search)?);
self.resolve_config_sort(items, &mut mode);
let writer = SqlWriter::new(self, items);
let writer = SqlWriter::new(self, item_type);
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
self.add_order(&mut sql, items, mode)?;
self.add_order(&mut sql, item_type, mode)?;
let mut stmt = self.storage.db.prepare(&sql)?;
let ids: Vec<_> = stmt
@ -140,14 +119,18 @@ impl Collection {
self.search(search, SortMode::NoOrder)
}
fn add_order(&mut self, sql: &mut String, items: SearchItems, mode: SortMode) -> Result<()> {
fn add_order(
&mut self,
sql: &mut String,
item_type: ReturnItemType,
mode: SortMode,
) -> Result<()> {
match mode {
SortMode::NoOrder => (),
SortMode::FromConfig => unreachable!(),
SortMode::Builtin { kind, reverse } => {
prepare_sort(self, kind)?;
SortMode::Builtin { column, reverse } => {
prepare_sort(self, column, item_type)?;
sql.push_str(" order by ");
write_order(sql, items, kind, reverse)?;
write_order(sql, item_type, column, reverse)?;
}
SortMode::Custom(order_clause) => {
sql.push_str(" order by ");
@ -166,11 +149,11 @@ impl Collection {
mode: SortMode,
) -> Result<usize> {
let top_node = Node::Group(parse(search)?);
let writer = SqlWriter::new(self, SearchItems::Cards);
let writer = SqlWriter::new(self, ReturnItemType::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)?;
self.add_order(&mut sql, ReturnItemType::Cards, mode)?;
if want_order {
self.storage
@ -186,34 +169,23 @@ impl Collection {
.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),
fn write_order(
sql: &mut String,
item_type: ReturnItemType,
column: Column,
reverse: bool,
) -> Result<()> {
let order = match item_type {
ReturnItemType::Cards => card_order_from_sort_column(column),
ReturnItemType::Notes => note_order_from_sort_column(column),
};
if order.is_empty() {
return Err(AnkiError::invalid_input(format!(
"Can't sort {:?} by {:?}.",
items, kind
item_type, column
)));
}
if reverse {
@ -229,60 +201,69 @@ fn write_order(sql: &mut String, items: SearchItems, kind: SortKind, reverse: bo
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!(
fn card_order_from_sort_column(column: Column) -> Cow<'static, str> {
match column {
Column::CardMod => "c.mod asc".into(),
Column::Cards => 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(),
Column::Deck => "(select pos from sort_order where did = c.did) asc".into(),
Column::Due => "c.type asc, c.due asc".into(),
Column::Ease => format!("c.type = {} asc, c.factor asc", CardType::New as i8).into(),
Column::Interval => "c.ivl asc".into(),
Column::Lapses => "c.lapses asc".into(),
Column::NoteCreation => "n.id asc, c.ord asc".into(),
Column::NoteMod => "n.mod asc, c.ord asc".into(),
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
Column::Reps => "c.reps asc".into(),
Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(),
Column::Tags => "n.tags asc".into(),
Column::Answer | Column::Custom | Column::Question => "".into(),
}
}
fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
match kind {
SortKind::NoteCards
| SortKind::NoteDue
| SortKind::NoteEase
| SortKind::NoteInterval
| SortKind::NoteLapses
| SortKind::NoteReps => "(select pos from sort_order where nid = n.id) asc".into(),
SortKind::NoteCreation => "n.id asc".into(),
SortKind::NoteField => "n.sfld collate nocase asc".into(),
SortKind::NoteMod => "n.mod asc".into(),
SortKind::NoteTags => "n.tags asc".into(),
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
_ => "".into(),
fn note_order_from_sort_column(column: Column) -> Cow<'static, str> {
match column {
Column::CardMod
| Column::Cards
| Column::Deck
| Column::Due
| Column::Ease
| Column::Interval
| Column::Lapses
| Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(),
Column::NoteCreation => "n.id asc".into(),
Column::NoteMod => "n.mod asc".into(),
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
Column::SortField => "n.sfld collate nocase asc".into(),
Column::Tags => "n.tags asc".into(),
Column::Answer | Column::Custom | Column::Question => "".into(),
}
}
fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
use SortKind::*;
let sql = match kind {
CardDeck => include_str!("deck_order.sql"),
CardTemplate => include_str!("template_order.sql"),
NoteCards => include_str!("note_cards_order.sql"),
NoteDue => include_str!("note_due_order.sql"),
NoteEase => include_str!("note_ease_order.sql"),
NoteInterval => include_str!("note_interval_order.sql"),
NoteLapses => include_str!("note_lapses_order.sql"),
NoteReps => include_str!("note_reps_order.sql"),
Notetype => include_str!("notetype_order.sql"),
_ => return Ok(()),
fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType) -> Result<()> {
let sql = match item_type {
ReturnItemType::Cards => match column {
Column::Cards => include_str!("template_order.sql"),
Column::Deck => include_str!("deck_order.sql"),
Column::Notetype => include_str!("notetype_order.sql"),
_ => return Ok(()),
},
ReturnItemType::Notes => match column {
Column::Cards => include_str!("note_cards_order.sql"),
Column::CardMod => include_str!("card_mod_order.sql"),
Column::Deck => include_str!("note_decks_order.sql"),
Column::Due => include_str!("note_due_order.sql"),
Column::Ease => include_str!("note_ease_order.sql"),
Column::Interval => include_str!("note_interval_order.sql"),
Column::Lapses => include_str!("note_lapses_order.sql"),
Column::Reps => include_str!("note_reps_order.sql"),
Column::Notetype => include_str!("notetype_order.sql"),
_ => return Ok(()),
},
};
col.storage.db.execute_batch(sql)?;

View File

@ -0,0 +1,18 @@
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
JOIN (
SELECT id,
row_number() OVER(
ORDER BY name
) AS pos
FROM decks
) decks ON cards.did = decks.id
GROUP BY nid
ORDER BY COUNT(DISTINCT did),
decks.pos;

View File

@ -3,7 +3,7 @@
use super::{
parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind},
SearchItems,
ReturnItemType,
};
use crate::{
card::{CardQueue, CardType},
@ -25,24 +25,24 @@ use std::{borrow::Cow, fmt::Write};
pub(crate) struct SqlWriter<'a> {
col: &'a mut Collection,
sql: String,
items: SearchItems,
item_type: ReturnItemType,
args: Vec<String>,
normalize_note_text: bool,
table: RequiredTable,
}
impl SqlWriter<'_> {
pub(crate) fn new(col: &mut Collection, items: SearchItems) -> SqlWriter<'_> {
pub(crate) fn new(col: &mut Collection, item_type: ReturnItemType) -> SqlWriter<'_> {
let normalize_note_text = col.get_bool(BoolKey::NormalizeNoteText);
let sql = String::new();
let args = vec![];
SqlWriter {
col,
sql,
items,
item_type,
args,
normalize_note_text,
table: items.required_table(),
table: item_type.required_table(),
}
}
@ -61,9 +61,9 @@ impl SqlWriter<'_> {
let sql = match self.table {
RequiredTable::Cards => "select c.id from cards c where ",
RequiredTable::Notes => "select n.id from notes n where ",
_ => match self.items {
SearchItems::Cards => "select c.id from cards c, notes n where c.nid=n.id and ",
SearchItems::Notes => {
_ => match self.item_type {
ReturnItemType::Cards => "select c.id from cards c, notes n where c.nid=n.id and ",
ReturnItemType::Notes => {
"select distinct n.id from cards c, notes n where c.nid=n.id and "
}
},
@ -588,7 +588,7 @@ mod test {
// shortcut
fn s(req: &mut Collection, search: &str) -> (String, Vec<String>) {
let node = Node::Group(parse(search).unwrap());
let mut writer = SqlWriter::new(req, SearchItems::Cards);
let mut writer = SqlWriter::new(req, ReturnItemType::Cards);
writer.table = RequiredTable::Notes.combine(node.required_table());
writer.write_node_to_sql(&node).unwrap();
(writer.sql, writer.args)