diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl
index 89755266c..b99e40952 100644
--- a/ftl/core/browsing.ftl
+++ b/ftl/core/browsing.ftl
@@ -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
diff --git a/pylib/.pylintrc b/pylib/.pylintrc
index 915320c1e..b734d4c91 100644
--- a/pylib/.pylintrc
+++ b/pylib/.pylintrc
@@ -4,6 +4,7 @@ persistent = no
[TYPECHECK]
ignored-classes=
+ BrowserColumns,
BrowserRow,
FormatTimespanIn,
AnswerCardIn,
diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 14dbbafb2..84977650a 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -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))
diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py
index 91d7f1bb0..56ada7e5f 100644
--- a/pylib/tests/test_find.py
+++ b/pylib/tests/test_find.py
@@ -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
diff --git a/qt/.pylintrc b/qt/.pylintrc
index b4809e76d..fb0800e47 100644
--- a/qt/.pylintrc
+++ b/qt/.pylintrc
@@ -6,6 +6,7 @@ ignore = forms,hooks_gen.py
[TYPECHECK]
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
ignored-classes=
+ BrowserColumns,
BrowserRow,
SearchNode,
Config,
diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py
index 88c0a09a8..9a0c97c21 100644
--- a/qt/aqt/browser.py
+++ b/qt/aqt/browser.py
@@ -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)
diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui
index 810724b33..9f499fe60 100644
--- a/qt/aqt/forms/browser.ui
+++ b/qt/aqt/forms/browser.ui
@@ -607,10 +607,10 @@
- browsing_toggle_cards_notes_mode
+ browsing_toggle_showing_cards_notes
- Ctrl+M
+ Alt+T
diff --git a/qt/aqt/table.py b/qt/aqt/table.py
index cc94b8f44..ea19fc379 100644
--- a/qt/aqt/table.py
+++ b/qt/aqt/table.py
@@ -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,
+ )
diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py
index 5b4f2b4ab..f7fdd61c9 100644
--- a/qt/tools/genhooks_gui.py
+++ b/qt/tools/genhooks_gui.py
@@ -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,
diff --git a/rslib/backend.proto b/rslib/backend.proto
index f7ba5c40d..7a55cf2a0 100644
--- a/rslib/backend.proto
+++ b/rslib/backend.proto
@@ -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;
diff --git a/rslib/src/backend/search/browser_table.rs b/rslib/src/backend/search/browser_table.rs
index cb405769a..7bcd74690 100644
--- a/rslib/src/backend/search/browser_table.rs
+++ b/rslib/src/backend/search/browser_table.rs
@@ -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 for Vec {
fn from(input: pb::StringList) -> Self {
- input.vals.into_iter().map(Into::into).collect()
- }
-}
-
-impl From 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 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 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 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()
}
}
diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs
index 5aaf25780..49a49d7f5 100644
--- a/rslib/src/backend/search/mod.rs
+++ b/rslib/src/backend/search/mod.rs
@@ -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 {
+ self.with_col(|col| Ok(col.all_browser_columns()))
+ }
+
+ fn set_active_browser_columns(&self, input: pb::StringList) -> Result {
+ 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 {
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 {
- 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 {
- self.with_col(|col| col.set_desktop_browser_note_columns(input.into()))?;
- Ok(().into())
- }
-}
-
-impl From 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