move remaining Filter button items into sidebar

- Closes #976
- Added helper to apply arbitrary colour to an icon.
- Fix #979 - low res icons in night mode.
- The icons and colours are not perfect - please feel free to send
through a PR if you can improve them.
- Convert colors dictionary into module consts, so we can
use code completion.
- Added "Edited Today" and "Due Tomorrow"
- Rename camelCase attribute to snake_case and tweak the wording
of some enum constants. We've already broken compatibility with the
major sidebar add-ons, so we may as well make these changes while we
can.
- Removed Filter button. Currently there is no exposed way to toggle
the Sidebar off - wonder if we still need it?
This commit is contained in:
Damien Elmes 2021-02-05 15:26:12 +10:00
parent bd730a012e
commit b8d67cdad5
22 changed files with 514 additions and 319 deletions

View file

@ -4,6 +4,7 @@ browsing-add-tags2 = Add Tags...
browsing-added-today = Added Today browsing-added-today = Added Today
browsing-addon = Add-on browsing-addon = Add-on
browsing-again-today = Again Today browsing-again-today = Again Today
browsing-edited-today = Edited Today
browsing-all-card-types = All Card Types browsing-all-card-types = All Card Types
browsing-all-fields = All Fields browsing-all-fields = All Fields
browsing-answer = Answer browsing-answer = Answer
@ -34,7 +35,6 @@ browsing-ease = Ease
browsing-end = End browsing-end = End
browsing-enter-tags-to-add = Enter tags to add: browsing-enter-tags-to-add = Enter tags to add:
browsing-enter-tags-to-delete = Enter tags to delete: browsing-enter-tags-to-delete = Enter tags to delete:
browsing-filter = Filter...
browsing-filtered = (filtered) browsing-filtered = (filtered)
browsing-find = <b>Find</b>: browsing-find = <b>Find</b>:
browsing-find-and-replace = Find and Replace browsing-find-and-replace = Find and Replace
@ -127,3 +127,8 @@ browsing-sidebar-tags = Tags
browsing-sidebar-notetypes = Note Types browsing-sidebar-notetypes = Note Types
browsing-sidebar-saved-searches = Saved Searches browsing-sidebar-saved-searches = Saved Searches
browsing-sidebar-save-current-search = Save Current Search browsing-sidebar-save-current-search = Save Current Search
browsing-sidebar-card-state = Card State
browsing-sidebar-flags = Flags
browsing-sidebar-recent = Recent
browsing-sidebar-due-today = Due Today
browsing-sidebar-due-tomorrow = Due Tomorrow

View file

@ -1,2 +0,0 @@
# True if a card is due/ready for review
filtering-is-due = Due

View file

@ -2,4 +2,5 @@ from typing import NoReturn
def assert_exhaustive(arg: NoReturn) -> NoReturn: def assert_exhaustive(arg: NoReturn) -> NoReturn:
"""The type definition will cause mypy to tell us if we've missed an enum case."""
raise Exception(f"unexpected arg received: {type(arg)} {arg}") raise Exception(f"unexpected arg received: {type(arg)} {arg}")

View file

@ -488,6 +488,7 @@ def _run(argv: Optional[List[str]] = None, exec: bool = True) -> Optional[AnkiAp
# opt in to full hidpi support? # opt in to full hidpi support?
if not os.environ.get("ANKI_NOHIGHDPI"): if not os.environ.get("ANKI_NOHIGHDPI"):
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough" os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough"

View file

@ -33,8 +33,6 @@ from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
TR, TR,
HelpPage, HelpPage,
MenuList,
SubMenu,
askUser, askUser,
disable_help_button, disable_help_button,
getTag, getTag,
@ -63,10 +61,6 @@ from aqt.utils import (
) )
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
# legacy add-on support
# pylint: disable=unused-import
from aqt.sidebar import SidebarItem, SidebarStage # isort: skip
@dataclass @dataclass
class FindDupesDialog: class FindDupesDialog:
@ -486,7 +480,6 @@ class Browser(QMainWindow):
# pylint: disable=unnecessary-lambda # pylint: disable=unnecessary-lambda
# actions # actions
f = self.form f = self.form
qconnect(f.filter.clicked, self.onFilterButton)
# edit # edit
qconnect(f.actionUndo.triggered, self.mw.onUndo) qconnect(f.actionUndo.triggered, self.mw.onUndo)
qconnect(f.actionInvertSelection.triggered, self.invertSelection) qconnect(f.actionInvertSelection.triggered, self.invertSelection)
@ -978,34 +971,15 @@ QTableView {{ gridline-color: {grid} }}
self.showSidebar() self.showSidebar()
self.sidebar.searchBar.setFocus() self.sidebar.searchBar.setFocus()
# legacy
def maybeRefreshSidebar(self) -> None:
self.sidebar.refresh()
def toggle_sidebar(self) -> None: def toggle_sidebar(self) -> None:
want_visible = not self.sidebarDockWidget.isVisible() want_visible = not self.sidebarDockWidget.isVisible()
self.sidebarDockWidget.setVisible(want_visible) self.sidebarDockWidget.setVisible(want_visible)
if want_visible: if want_visible:
self.sidebar.refresh() self.sidebar.refresh()
# Filter button and sidebar helpers # Sidebar helpers
###################################################################### ######################################################################
def onFilterButton(self) -> None:
ml = MenuList()
ml.addChild(self._todayFilters())
ml.addChild(self._cardStateFilters())
ml.addSeparator()
toggle_sidebar = QAction(tr(TR.BROWSING_SIDEBAR))
qconnect(toggle_sidebar.triggered, self.toggle_sidebar)
toggle_sidebar.setCheckable(True)
toggle_sidebar.setChecked(self.sidebarDockWidget.isVisible())
ml.addChild(toggle_sidebar)
ml.popupOver(self.form.filter)
def update_search(self, *terms: Union[str, SearchTerm]) -> None: def update_search(self, *terms: Union[str, SearchTerm]) -> None:
"""Modify the current search string based on modified keys, then refresh.""" """Modify the current search string based on modified keys, then refresh."""
try: try:
@ -1030,84 +1004,6 @@ QTableView {{ gridline-color: {grid} }}
def setFilter(self, *terms: str) -> None: def setFilter(self, *terms: str) -> None:
self.set_filter_then_search(*terms) self.set_filter_then_search(*terms)
def _simpleFilters(self, items: Sequence[Tuple[str, SearchTerm]]) -> MenuList:
ml = MenuList()
for row in items:
if row is None:
ml.addSeparator()
else:
label, filter_name = row
ml.addItem(label, self.sidebar._filter_func(filter_name))
return ml
def _todayFilters(self) -> SubMenu:
subm = SubMenu(tr(TR.BROWSING_TODAY))
subm.addChild(
self._simpleFilters(
(
(tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)),
(
tr(TR.BROWSING_STUDIED_TODAY),
SearchTerm(rated=SearchTerm.Rated(days=1)),
),
(
tr(TR.BROWSING_AGAIN_TODAY),
SearchTerm(
rated=SearchTerm.Rated(
days=1, rating=SearchTerm.RATING_AGAIN
)
),
),
)
)
)
return subm
def _cardStateFilters(self) -> SubMenu:
subm = SubMenu(tr(TR.BROWSING_CARD_STATE))
subm.addChild(
self._simpleFilters(
(
(
tr(TR.ACTIONS_NEW),
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
),
(
tr(TR.SCHEDULING_LEARNING),
SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN),
),
(
tr(TR.SCHEDULING_REVIEW),
SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW),
),
(
tr(TR.FILTERING_IS_DUE),
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE),
),
None,
(
tr(TR.BROWSING_SUSPENDED),
SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED),
),
(
tr(TR.BROWSING_BURIED),
SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED),
),
None,
(tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.FLAG_RED)),
(
tr(TR.ACTIONS_ORANGE_FLAG),
SearchTerm(flag=SearchTerm.FLAG_ORANGE),
),
(tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.FLAG_GREEN)),
(tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.FLAG_BLUE)),
(tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.FLAG_NONE)),
(tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.FLAG_ANY)),
)
)
)
return subm
# Info # Info
###################################################################### ######################################################################

1
qt/aqt/colors.py Symbolic link
View file

@ -0,0 +1 @@
../../bazel-bin/qt/aqt/colors.py

View file

@ -91,7 +91,7 @@
<property name="verticalSpacing"> <property name="verticalSpacing">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="1"> <item row="0" column="0">
<widget class="QComboBox" name="searchEdit"> <widget class="QComboBox" name="searchEdit">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -107,13 +107,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0">
<widget class="QPushButton" name="filter">
<property name="text">
<string>BROWSING_FILTER</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>
@ -151,12 +144,12 @@
<attribute name="horizontalHeaderCascadingSectionResizes"> <attribute name="horizontalHeaderCascadingSectionResizes">
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize"> <attribute name="horizontalHeaderMinimumSectionSize">
<number>20</number> <number>20</number>
</attribute> </attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0"> <attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool> <bool>true</bool>
</attribute> </attribute>
@ -216,7 +209,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>750</width> <width>750</width>
<height>21</height> <height>24</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuEdit"> <widget class="QMenu" name="menuEdit">

View file

@ -1,11 +1,14 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>icons/anki.png</file> <file>icons/anki.png</file>
<file>icons/tag.svg</file> <file>icons/tag.svg</file>
<file>icons/deck.svg</file> <file>icons/deck.svg</file>
<file>icons/notetype.svg</file> <file>icons/notetype.svg</file>
<file>icons/heart.svg</file> <file>icons/heart.svg</file>
<file>icons/collection.svg</file> <file>icons/collection.svg</file>
<file>icons/media-record.png</file> <file>icons/media-record.png</file>
</qresource> <file>icons/clock.svg</file>
<file>icons/card-state.svg</file>
<file>icons/flag.svg</file>
</qresource>
</RCC> </RCC>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 61 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-78)">
<g id="card-state" transform="matrix(0.938173,0,0,0.938173,-241.492,-55.2956)">
<rect x="257.964" y="142.08" width="63.954" height="63.954" style="fill:none;"/>
<g transform="matrix(5.59598,0,0,5.59598,110.312,5.90138)">
<circle cx="32" cy="30" r="4"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 832 B

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(1,0,0,1,-70,-78)">
<g id="clock" transform="matrix(0.938173,0,0,0.938173,-172.014,-55.2956)">
<rect x="257.964" y="142.08" width="63.954" height="63.954" style="fill:none;"/>
<g transform="matrix(0.400362,0,0,0.400362,168.807,-30.4403)">
<g transform="matrix(1,0,0,1,-14.7441,-15.49)">
<path d="M317.306,450.972C358.893,450.972 392.605,484.684 392.605,526.271C392.605,567.858 358.893,601.571 317.306,601.571C275.719,601.571 242.006,567.858 242.006,526.271C242.006,484.684 275.719,450.972 317.306,450.972ZM321.122,451.963L351.335,460.058L347.8,473.251L357.458,463.593L379.575,485.711L369.917,495.369L383.11,491.834L391.206,522.047L378.013,525.582L391.206,529.117L383.11,559.33L369.917,555.794L379.575,565.452L357.458,587.57L347.8,577.912L351.335,591.105L321.122,599.2L317.587,586.007L314.052,599.2L283.839,591.105L287.374,577.912L277.716,587.57L255.599,565.452L265.257,555.794L252.064,559.33L243.968,529.117L257.161,525.582L243.968,522.047L252.064,491.834L265.257,495.369L255.599,485.711L277.716,463.593L287.374,473.251L283.839,460.058L314.052,451.963L317.587,465.156L321.122,451.963Z" style="fill:rgb(115,115,115);stroke:black;stroke-width:5.55px;"/>
</g>
<g transform="matrix(1,0,0,1,-0.0402259,-5.42135)">
<path d="M302.583,519.144C302.583,503.542 301.608,474.556 301.608,458.936" style="fill:none;stroke:black;stroke-width:11.09px;"/>
</g>
<g transform="matrix(1,0,0,1,-1.89244,1.87233)">
<path d="M304.779,510.287L329.081,496.966" style="fill:none;stroke:black;stroke-width:11.09px;"/>
</g>
<path d="M302.707,451.966L307.7,461.953L297.714,461.953L302.707,451.966Z" style="fill:rgb(115,115,115);stroke:black;stroke-width:11.09px;"/>
<g transform="matrix(0.487004,0.8734,-0.8734,0.487004,328.596,498.57)">
<path d="M0,-5.016L5.016,5.016L-5.016,5.016L0,-5.016Z" style="fill:rgb(115,115,115);stroke:black;stroke-width:11.09px;"/>
</g>
<circle cx="303.526" cy="511.396" r="3.888" style="fill:rgb(115,115,115);stroke:black;stroke-width:11.09px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 60 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="flag" transform="matrix(1,0,0,1.06667,-268.333,0)">
<rect x="268.333" y="0" width="60" height="60" style="fill:none;"/>
<g transform="matrix(0.8,0,0,0.973558,207.333,-4.03846)">
<rect x="85" y="8" width="5" height="52"/>
</g>
<g transform="matrix(0.914894,0,0,0.837054,196.993,-1.27232)">
<path d="M137,6C137,6 130.12,4.079 122.791,4.88C114.785,5.755 112.375,9.441 103.116,9.36C98.955,9.324 90,6 90,6L90,34C90,34 98.617,37.72 103.116,37.36C112.889,36.577 119.329,32.32 124.977,31.76C129.034,31.358 137,34 137,34L137,6Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -5,7 +5,7 @@
from __future__ import annotations from __future__ import annotations
from concurrent.futures import Future from concurrent.futures import Future
from enum import Enum from enum import Enum, auto
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast
import aqt import aqt
@ -13,11 +13,12 @@ from anki.collection import ConfigBoolKey, SearchTerm
from anki.decks import DeckTreeNode from anki.decks import DeckTreeNode
from anki.errors import DeckRenameError, InvalidInput from anki.errors import DeckRenameError, InvalidInput
from anki.tags import TagTreeNode from anki.tags import TagTreeNode
from aqt import gui_hooks from anki.types import assert_exhaustive
from aqt import colors, gui_hooks
from aqt.main import ResetReason from aqt.main import ResetReason
from aqt.models import Models from aqt.models import Models
from aqt.qt import * from aqt.qt import *
from aqt.theme import theme_manager from aqt.theme import ColoredIcon, theme_manager
from aqt.utils import ( from aqt.utils import (
TR, TR,
askUser, askUser,
@ -30,39 +31,51 @@ from aqt.utils import (
class SidebarItemType(Enum): class SidebarItemType(Enum):
ROOT = 0 ROOT = auto()
COLLECTION = 1 SAVED_SEARCH_ROOT = auto()
CURRENT_DECK = 2 SAVED_SEARCH = auto()
SAVED_SEARCH = 3 RECENT_ROOT = auto()
FILTER = 3 # legacy alias for SAVED_SEARCH RECENT = auto()
DECK = 4 CARD_STATE_ROOT = auto()
NOTETYPE = 5 CARD_STATE = auto()
TAG = 6 FLAG_ROOT = auto()
CUSTOM = 7 FLAG = auto()
TEMPLATE = 8 DECK_ROOT = auto()
SAVED_SEARCH_ROOT = 9 DECK = auto()
DECK_ROOT = 10 NOTETYPE_ROOT = auto()
NOTETYPE_ROOT = 11 NOTETYPE = auto()
TAG_ROOT = 12 NOTETYPE_TEMPLATE = auto()
TAG_ROOT = auto()
TAG = auto()
CUSTOM = auto()
@staticmethod
def section_roots() -> Iterable[SidebarItemType]:
return (type for type in SidebarItemType if type.name.endswith("_ROOT"))
def is_section_root(self) -> bool:
return self in self.section_roots()
# used by an add-on hook
class SidebarStage(Enum): class SidebarStage(Enum):
ROOT = 0 ROOT = auto()
STANDARD = 1 SAVED_SEARCHES = auto()
FAVORITES = 2 RECENT = auto()
DECKS = 3 SCHEDULING = auto()
MODELS = 4 FLAGS = auto()
TAGS = 5 DECKS = auto()
NOTETYPES = auto()
TAGS = auto()
class SidebarItem: class SidebarItem:
def __init__( def __init__(
self, self,
name: str, name: str,
icon: str, icon: Union[str, ColoredIcon],
onClick: Callable[[], None] = None, on_click: Callable[[], None] = None,
onExpanded: Callable[[bool], None] = None, on_expanded: Callable[[bool], None] = None,
expanded: bool = False, expanded: bool = False,
item_type: SidebarItemType = SidebarItemType.CUSTOM, item_type: SidebarItemType = SidebarItemType.CUSTOM,
id: int = 0, id: int = 0,
@ -75,39 +88,47 @@ class SidebarItem:
self.icon = icon self.icon = icon
self.item_type = item_type self.item_type = item_type
self.id = id self.id = id
self.onClick = onClick self.on_click = on_click
self.onExpanded = onExpanded self.on_expanded = on_expanded
self.expanded = expanded
self.children: List["SidebarItem"] = [] self.children: List["SidebarItem"] = []
self.parentItem: Optional["SidebarItem"] = None
self.tooltip: Optional[str] = None self.tooltip: Optional[str] = None
self.row_in_parent: Optional[int] = None self._parent_item: Optional["SidebarItem"] = None
self._is_expanded = expanded
self._row_in_parent: Optional[int] = None
self._search_matches_self = False self._search_matches_self = False
self._search_matches_child = False self._search_matches_child = False
def addChild(self, cb: "SidebarItem") -> None: def add_child(self, cb: "SidebarItem") -> None:
self.children.append(cb) self.children.append(cb)
cb.parentItem = self cb._parent_item = self
def rowForChild(self, child: "SidebarItem") -> Optional[int]: def add_simple(
try: self,
return self.children.index(child) name: Union[str, TR.V],
except ValueError: icon: Union[str, ColoredIcon],
return None type: SidebarItemType,
on_click: Callable[[], None],
) -> SidebarItem:
"Add child sidebar item, and return it."
if not isinstance(name, str):
name = tr(name)
item = SidebarItem(
name=name,
icon=icon,
on_click=on_click,
item_type=type,
)
self.add_child(item)
return item
def is_expanded(self, searching: bool) -> bool: def is_expanded(self, searching: bool) -> bool:
if not searching: if not searching:
return self.expanded return self._is_expanded
else: else:
if self._search_matches_child: if self._search_matches_child:
return True return True
# if search matches top level, expand children one level # if search matches top level, expand children one level
return self._search_matches_self and self.item_type in ( return self._search_matches_self and self.item_type.is_section_root()
SidebarItemType.SAVED_SEARCH_ROOT,
SidebarItemType.DECK_ROOT,
SidebarItemType.NOTETYPE_ROOT,
SidebarItemType.TAG_ROOT,
)
def is_highlighted(self) -> bool: def is_highlighted(self) -> bool:
return self._search_matches_self return self._search_matches_self
@ -130,7 +151,7 @@ class SidebarModel(QAbstractItemModel):
def _cache_rows(self, node: SidebarItem) -> None: def _cache_rows(self, node: SidebarItem) -> None:
"Cache index of children in parent." "Cache index of children in parent."
for row, item in enumerate(node.children): for row, item in enumerate(node.children):
item.row_in_parent = row item._row_in_parent = row
self._cache_rows(item) self._cache_rows(item)
def item_for_index(self, idx: QModelIndex) -> SidebarItem: def item_for_index(self, idx: QModelIndex) -> SidebarItem:
@ -172,12 +193,12 @@ class SidebarModel(QAbstractItemModel):
return QModelIndex() return QModelIndex()
childItem: SidebarItem = child.internalPointer() childItem: SidebarItem = child.internalPointer()
parentItem = childItem.parentItem parentItem = childItem._parent_item
if parentItem is None or parentItem == self.root: if parentItem is None or parentItem == self.root:
return QModelIndex() return QModelIndex()
row = parentItem.row_in_parent row = parentItem._row_in_parent
return self.createIndex(row, 0, parentItem) return self.createIndex(row, 0, parentItem)
@ -216,30 +237,6 @@ class SidebarModel(QAbstractItemModel):
return cast(Qt.ItemFlags, flags) return cast(Qt.ItemFlags, flags)
# Helpers
######################################################################
def iconFromRef(self, iconRef: str) -> QIcon:
print("iconFromRef() deprecated")
return theme_manager.icon_from_resources(iconRef)
def expand_where_necessary(
model: SidebarModel,
tree: QTreeView,
parent: Optional[QModelIndex] = None,
searching: bool = False,
) -> None:
parent = parent or QModelIndex()
for row in range(model.rowCount(parent)):
idx = model.index(row, 0, parent)
if not idx.isValid():
continue
expand_where_necessary(model, tree, idx, searching)
if item := model.item_for_index(idx):
if item.is_expanded(searching):
tree.setExpanded(idx, True)
class SidebarSearchBar(QLineEdit): class SidebarSearchBar(QLineEdit):
def __init__(self, sidebar: SidebarTreeView) -> None: def __init__(self, sidebar: SidebarTreeView) -> None:
@ -308,8 +305,8 @@ class SidebarTreeView(QTreeView):
self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragDropOverwriteMode(False) self.setDragDropOverwriteMode(False)
qconnect(self.expanded, self.onExpansion) qconnect(self.expanded, self._on_expansion)
qconnect(self.collapsed, self.onCollapse) qconnect(self.collapsed, self._on_collapse)
# match window background color # match window background color
bgcolor = QPalette().window().color().name() bgcolor = QPalette().window().color().name()
@ -334,7 +331,7 @@ class SidebarTreeView(QTreeView):
if self.current_search: if self.current_search:
self.search_for(self.current_search) self.search_for(self.current_search)
else: else:
expand_where_necessary(model, self) self._expand_where_necessary(model)
self.mw.taskman.run_in_background(self._root_tree, on_done) self.mw.taskman.run_in_background(self._root_tree, on_done)
@ -349,7 +346,26 @@ class SidebarTreeView(QTreeView):
# start from a collapsed state, as it's faster # start from a collapsed state, as it's faster
self.collapseAll() self.collapseAll()
self.setColumnHidden(0, not self.model().search(text)) self.setColumnHidden(0, not self.model().search(text))
expand_where_necessary(self.model(), self, searching=True) self._expand_where_necessary(self.model(), searching=True)
def _expand_where_necessary(
self,
model: SidebarModel,
parent: Optional[QModelIndex] = None,
searching: bool = False,
) -> None:
parent = parent or QModelIndex()
for row in range(model.rowCount(parent)):
idx = model.index(row, 0, parent)
if not idx.isValid():
continue
self._expand_where_necessary(model, idx, searching)
if item := model.item_for_index(idx):
if item.is_expanded(searching):
self.setExpanded(idx, True)
# Qt API
###########
def drawRow( def drawRow(
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
@ -369,6 +385,19 @@ class SidebarTreeView(QTreeView):
if self.handle_drag_drop(source_items, target_item): if self.handle_drag_drop(source_items, target_item):
event.acceptProposedAction() event.acceptProposedAction()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton:
self._on_click_current()
def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self._on_click_current()
else:
super().keyPressEvent(event)
###########
def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool: def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool:
if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT): if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
return self._handle_drag_drop_decks(sources, target) return self._handle_drag_drop_decks(sources, target)
@ -433,74 +462,70 @@ class SidebarTreeView(QTreeView):
self.browser.editor.saveNow(on_save) self.browser.editor.saveNow(on_save)
return True return True
def onClickCurrent(self) -> None: def _on_click_current(self) -> None:
idx = self.currentIndex() idx = self.currentIndex()
if item := self.model().item_for_index(idx): if item := self.model().item_for_index(idx):
if item.onClick: if item.on_click:
item.onClick() item.on_click()
def mouseReleaseEvent(self, event: QMouseEvent) -> None: def _on_expansion(self, idx: QModelIndex) -> None:
super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton:
self.onClickCurrent()
def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.onClickCurrent()
else:
super().keyPressEvent(event)
def onExpansion(self, idx: QModelIndex) -> None:
if self.current_search: if self.current_search:
return return
self._onExpansionChange(idx, True) self._on_expand_or_collapse(idx, True)
def onCollapse(self, idx: QModelIndex) -> None: def _on_collapse(self, idx: QModelIndex) -> None:
if self.current_search: if self.current_search:
return return
self._onExpansionChange(idx, False) self._on_expand_or_collapse(idx, False)
def _onExpansionChange(self, idx: QModelIndex, expanded: bool) -> None: def _on_expand_or_collapse(self, idx: QModelIndex, expanded: bool) -> None:
item = self.model().item_for_index(idx) item = self.model().item_for_index(idx)
if item and item.expanded != expanded: if item and item._is_expanded != expanded:
item.expanded = expanded item._is_expanded = expanded
if item.onExpanded: if item.on_expanded:
item.onExpanded(expanded) item.on_expanded(expanded)
# Tree building # Tree building
########################### ###########################
def _root_tree(self) -> SidebarItem: def _root_tree(self) -> SidebarItem:
root = SidebarItem("", "", item_type=SidebarItemType.ROOT) root: Optional[SidebarItem] = None
handled = gui_hooks.browser_will_build_tree( for stage in SidebarStage:
False, root, SidebarStage.ROOT, self if stage == SidebarStage.ROOT:
) root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
if handled:
return root
for stage, builder in zip(
list(SidebarStage)[1:],
(
self._commonly_used_tree,
self._saved_searches_tree,
self._deck_tree,
self._notetype_tree,
self._tag_tree,
),
):
handled = gui_hooks.browser_will_build_tree(False, root, stage, self) handled = gui_hooks.browser_will_build_tree(False, root, stage, self)
if not handled and builder: if not handled:
builder(root) self._build_stage(root, stage)
return root return root
def _build_stage(self, root: SidebarItem, stage: SidebarStage) -> None:
if stage is SidebarStage.SAVED_SEARCHES:
self._saved_searches_tree(root)
elif stage is SidebarStage.SCHEDULING:
self._card_state_tree(root)
elif stage is SidebarStage.RECENT:
self._recent_tree(root)
elif stage is SidebarStage.FLAGS:
self._flags_tree(root)
elif stage is SidebarStage.DECKS:
self._deck_tree(root)
elif stage is SidebarStage.NOTETYPES:
self._notetype_tree(root)
elif stage is SidebarStage.TAGS:
self._tag_tree(root)
elif stage is SidebarStage.ROOT:
pass
else:
assert_exhaustive(stage)
def _section_root( def _section_root(
self, self,
*, *,
root: SidebarItem, root: SidebarItem,
name: TR.V, name: TR.V,
icon: str, icon: Union[str, ColoredIcon],
collapse_key: ConfigBoolKey.V, collapse_key: ConfigBoolKey.V,
type: Optional[SidebarItemType] = None, type: Optional[SidebarItemType] = None,
) -> SidebarItem: ) -> SidebarItem:
@ -510,29 +535,19 @@ class SidebarTreeView(QTreeView):
top = SidebarItem( top = SidebarItem(
tr(name), tr(name),
icon, icon,
onExpanded=update, on_expanded=update,
expanded=not self.col.get_config_bool(collapse_key), expanded=not self.col.get_config_bool(collapse_key),
item_type=type, item_type=type,
) )
root.addChild(top) root.add_child(top)
return top return top
def _commonly_used_tree(self, root: SidebarItem) -> None: def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
item = SidebarItem( return lambda: self.browser.update_search(self.col.build_search_string(*terms))
tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg", # Tree: Saved Searches
self._filter_func(), ###########################
item_type=SidebarItemType.COLLECTION,
)
root.addChild(item)
item = SidebarItem(
tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg",
self._filter_func(SearchTerm(deck="current")),
item_type=SidebarItemType.CURRENT_DECK,
)
root.addChild(item)
def _saved_searches_tree(self, root: SidebarItem) -> None: def _saved_searches_tree(self, root: SidebarItem) -> None:
icon = ":/icons/heart.svg" icon = ":/icons/heart.svg"
@ -542,23 +557,187 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES, name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES,
icon=icon, icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_FAVORITES, collapse_key=ConfigBoolKey.COLLAPSE_SAVED_SEARCHES,
type=SidebarItemType.SAVED_SEARCH_ROOT, type=SidebarItemType.SAVED_SEARCH_ROOT,
) )
def on_click() -> None: def on_click() -> None:
self.show_context_menu(root, None) self.show_context_menu(root, None)
root.onClick = on_click root.on_click = on_click
for name, filt in sorted(saved.items()): for name, filt in sorted(saved.items()):
item = SidebarItem( item = SidebarItem(
name, name,
icon, icon,
self._filter_func(filt), self._filter_func(filt),
item_type=SidebarItemType.FILTER, item_type=SidebarItemType.SAVED_SEARCH,
) )
root.addChild(item) root.add_child(item)
# Tree: Recent
###########################
def _recent_tree(self, root: SidebarItem) -> None:
icon = ":/icons/clock.svg"
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_RECENT,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_RECENT,
type=SidebarItemType.FLAG_ROOT,
)
type = SidebarItemType.FLAG
search = self._filter_func
root.add_simple(
TR.BROWSING_CURRENT_DECK,
icon=icon,
type=type,
on_click=search(SearchTerm(deck="current")),
)
root.add_simple(
name=TR.BROWSING_SIDEBAR_DUE_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)),
)
root.add_simple(
name=TR.BROWSING_ADDED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(added_in_days=1)),
)
root.add_simple(
name=TR.BROWSING_EDITED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(edited_in_days=1)),
)
root.add_simple(
name=TR.BROWSING_STUDIED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(rated=SearchTerm.Rated(days=1))),
)
root.add_simple(
name=TR.BROWSING_AGAIN_TODAY,
icon=icon,
type=type,
on_click=search(
SearchTerm(
rated=SearchTerm.Rated(days=1, rating=SearchTerm.RATING_AGAIN)
)
),
)
root.add_simple(
name=TR.BROWSING_SIDEBAR_DUE_TOMORROW,
icon=icon,
type=type,
on_click=search(SearchTerm(due_in_days=1)),
)
# Tree: Card State
###########################
def _card_state_tree(self, root: SidebarItem) -> None:
icon = ColoredIcon(path=":/icons/card-state.svg", color=colors.DISABLED)
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_CARD_STATE,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_CARD_STATE,
type=SidebarItemType.CARD_STATE_ROOT,
)
type = SidebarItemType.CARD_STATE
search = self._filter_func
root.add_simple(
TR.ACTIONS_NEW,
icon=icon.with_color(colors.NEW_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)),
)
root.add_simple(
name=TR.SCHEDULING_LEARNING,
icon=icon.with_color(colors.LEARN_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN)),
)
root.add_simple(
name=TR.SCHEDULING_REVIEW,
icon=icon.with_color(colors.REVIEW_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW)),
)
root.add_simple(
name=TR.BROWSING_SUSPENDED,
icon=icon.with_color(colors.SUSPENDED_FG),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED)),
)
root.add_simple(
name=TR.BROWSING_BURIED,
icon=icon.with_color(colors.BURIED_FG),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED)),
)
# Tree: Flags
###########################
def _flags_tree(self, root: SidebarItem) -> None:
icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED)
root = self._section_root(
root=root,
name=TR.BROWSING_SIDEBAR_FLAGS,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_FLAGS,
type=SidebarItemType.FLAG_ROOT,
)
type = SidebarItemType.FLAG
search = self._filter_func
root.add_simple(
TR.ACTIONS_RED_FLAG,
icon=icon.with_color(colors.FLAG1_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_RED)),
)
root.add_simple(
TR.ACTIONS_ORANGE_FLAG,
icon=icon.with_color(colors.FLAG2_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_ORANGE)),
)
root.add_simple(
TR.ACTIONS_GREEN_FLAG,
icon=icon.with_color(colors.FLAG3_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_GREEN)),
)
root.add_simple(
TR.ACTIONS_BLUE_FLAG,
icon=icon.with_color(colors.FLAG4_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_BLUE)),
)
root.add_simple(
TR.BROWSING_ANY_FLAG,
icon=icon.with_color(colors.TEXT_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_ANY)),
)
root.add_simple(
TR.BROWSING_NO_FLAG,
icon=icon.with_color(colors.DISABLED),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_NONE)),
)
# Tree: Tags
###########################
def _tag_tree(self, root: SidebarItem) -> None: def _tag_tree(self, root: SidebarItem) -> None:
icon = ":/icons/tag.svg" icon = ":/icons/tag.svg"
@ -583,7 +762,7 @@ class SidebarTreeView(QTreeView):
item_type=SidebarItemType.TAG, item_type=SidebarItemType.TAG,
full_name=head + node.name, full_name=head + node.name,
) )
root.addChild(item) root.add_child(item)
newhead = head + node.name + "::" newhead = head + node.name + "::"
render(item, node.children, newhead) render(item, node.children, newhead)
@ -597,6 +776,9 @@ class SidebarTreeView(QTreeView):
) )
render(root, tree.children) render(root, tree.children)
# Tree: Decks
###########################
def _deck_tree(self, root: SidebarItem) -> None: def _deck_tree(self, root: SidebarItem) -> None:
icon = ":/icons/deck.svg" icon = ":/icons/deck.svg"
@ -619,7 +801,7 @@ class SidebarTreeView(QTreeView):
id=node.deck_id, id=node.deck_id,
full_name=head + node.name, full_name=head + node.name,
) )
root.addChild(item) root.add_child(item)
newhead = head + node.name + "::" newhead = head + node.name + "::"
render(item, node.children, newhead) render(item, node.children, newhead)
@ -633,6 +815,9 @@ class SidebarTreeView(QTreeView):
) )
render(root, tree.children) render(root, tree.children)
# Tree: Notetypes
###########################
def _notetype_tree(self, root: SidebarItem) -> None: def _notetype_tree(self, root: SidebarItem) -> None:
icon = ":/icons/notetype.svg" icon = ":/icons/notetype.svg"
root = self._section_root( root = self._section_root(
@ -659,15 +844,12 @@ class SidebarTreeView(QTreeView):
self._filter_func( self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c) SearchTerm(note=nt["name"]), SearchTerm(template=c)
), ),
item_type=SidebarItemType.TEMPLATE, item_type=SidebarItemType.NOTETYPE_TEMPLATE,
full_name=nt["name"] + "::" + tmpl["name"], full_name=nt["name"] + "::" + tmpl["name"],
) )
item.addChild(child) item.add_child(child)
root.addChild(item) root.add_child(item)
def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
return lambda: self.browser.update_search(self.col.build_search_string(*terms))
# Context menu actions # Context menu actions
########################### ###########################
@ -735,7 +917,7 @@ class SidebarTreeView(QTreeView):
lambda: set_children_collapsed(True), lambda: set_children_collapsed(True),
) )
def rename_deck(self, item: "aqt.browser.SidebarItem") -> None: def rename_deck(self, item: SidebarItem) -> None:
deck = self.mw.col.decks.get(item.id) deck = self.mw.col.decks.get(item.id)
old_name = deck["name"] old_name = deck["name"]
new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name) new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name)
@ -751,10 +933,10 @@ class SidebarTreeView(QTreeView):
self.refresh() self.refresh()
self.mw.deckBrowser.refresh() self.mw.deckBrowser.refresh()
def remove_tag(self, item: "aqt.browser.SidebarItem") -> None: def remove_tag(self, item: SidebarItem) -> None:
self.browser.editor.saveNow(lambda: self._remove_tag(item)) self.browser.editor.saveNow(lambda: self._remove_tag(item))
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None: def _remove_tag(self, item: SidebarItem) -> None:
old_name = item.full_name old_name = item.full_name
def do_remove() -> None: def do_remove() -> None:
@ -771,10 +953,10 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset() self.browser.model.beginReset()
self.mw.taskman.run_in_background(do_remove, on_done) self.mw.taskman.run_in_background(do_remove, on_done)
def rename_tag(self, item: "aqt.browser.SidebarItem") -> None: def rename_tag(self, item: SidebarItem) -> None:
self.browser.editor.saveNow(lambda: self._rename_tag(item)) self.browser.editor.saveNow(lambda: self._rename_tag(item))
def _rename_tag(self, item: "aqt.browser.SidebarItem") -> None: def _rename_tag(self, item: SidebarItem) -> None:
old_name = item.full_name old_name = item.full_name
new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name)
if new_name == old_name or not new_name: if new_name == old_name or not new_name:
@ -799,10 +981,10 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset() self.browser.model.beginReset()
self.mw.taskman.run_in_background(do_rename, on_done) self.mw.taskman.run_in_background(do_rename, on_done)
def delete_deck(self, item: "aqt.browser.SidebarItem") -> None: def delete_deck(self, item: SidebarItem) -> None:
self.browser.editor.saveNow(lambda: self._delete_deck(item)) self.browser.editor.saveNow(lambda: self._delete_deck(item))
def _delete_deck(self, item: "aqt.browser.SidebarItem") -> None: def _delete_deck(self, item: SidebarItem) -> None:
did = item.id did = item.id
if self.mw.deckBrowser.ask_delete_deck(did): if self.mw.deckBrowser.ask_delete_deck(did):
@ -820,7 +1002,7 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset() self.browser.model.beginReset()
self.mw.taskman.run_in_background(do_delete, on_done) self.mw.taskman.run_in_background(do_delete, on_done)
def remove_saved_search(self, item: "aqt.browser.SidebarItem") -> None: def remove_saved_search(self, item: SidebarItem) -> None:
name = item.name name = item.name
if not askUser(tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=name)): if not askUser(tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=name)):
return return
@ -829,7 +1011,7 @@ class SidebarTreeView(QTreeView):
self.col.set_config("savedFilters", conf) self.col.set_config("savedFilters", conf)
self.refresh() self.refresh()
def rename_saved_search(self, item: "aqt.browser.SidebarItem") -> None: def rename_saved_search(self, item: SidebarItem) -> None:
old = item.name old = item.name
conf = self.col.get_config("savedFilters") conf = self.col.get_config("savedFilters")
try: try:
@ -860,7 +1042,7 @@ class SidebarTreeView(QTreeView):
self.col.set_config("savedFilters", conf) self.col.set_config("savedFilters", conf)
self.refresh() self.refresh()
def manage_notetype(self, item: "aqt.browser.SidebarItem") -> None: def manage_notetype(self, item: SidebarItem) -> None:
Models( Models(
self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id
) )

View file

@ -2,14 +2,32 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import platform import platform
from typing import Dict, Optional from dataclasses import dataclass
from typing import Dict, Optional, Tuple, Union
from anki.utils import isMac from anki.utils import isMac
from aqt import QApplication, gui_hooks, isWin from aqt import QApplication, colors, gui_hooks, isWin
from aqt.colors import colors
from aqt.platform import set_dark_mode from aqt.platform import set_dark_mode
from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt
@dataclass
class ColoredIcon:
path: str
# (day, night)
color: Tuple[str, str]
def current_color(self, night_mode: bool) -> str:
if night_mode:
return self.color[1]
else:
return self.color[0]
def with_color(self, color: Tuple[str, str]) -> ColoredIcon:
return ColoredIcon(path=self.path, color=color)
class ThemeManager: class ThemeManager:
@ -43,22 +61,39 @@ class ThemeManager:
night_mode = property(get_night_mode, set_night_mode) night_mode = property(get_night_mode, set_night_mode)
def icon_from_resources(self, path: str) -> QIcon: def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon:
"Fetch icon from Qt resources, and invert if in night mode." "Fetch icon from Qt resources, and invert if in night mode."
if self.night_mode: if self.night_mode:
cache = self._icon_cache_light cache = self._icon_cache_light
else: else:
cache = self._icon_cache_dark cache = self._icon_cache_dark
icon = cache.get(path)
if isinstance(path, str):
key = path
else:
key = f"{path.path}-{path.color}"
icon = cache.get(key)
if icon: if icon:
return icon return icon
icon = QIcon(path) if isinstance(path, str):
# default black/white
if self.night_mode: icon = QIcon(path)
img = icon.pixmap(self._icon_size, self._icon_size).toImage() if self.night_mode:
img.invertPixels() img = icon.pixmap(self._icon_size, self._icon_size).toImage()
icon = QIcon(QPixmap(img)) img.invertPixels()
icon = QIcon(QPixmap(img))
else:
# specified colours
icon = QIcon(path.path)
img = icon.pixmap(16)
painter = QPainter(img)
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
painter.fillRect(img.rect(), QColor(path.current_color(self.night_mode)))
painter.end()
icon = QIcon(img)
return icon
return cache.setdefault(path, icon) return cache.setdefault(path, icon)
@ -94,10 +129,10 @@ class ThemeManager:
Returns the color as a string hex code or color name.""" Returns the color as a string hex code or color name."""
idx = 1 if self.night_mode else 0 idx = 1 if self.night_mode else 0
c = colors.get(key)
if c is None: key = key.replace("-", "_").upper()
raise Exception("no such color:", key)
return c[idx] return getattr(colors, key)[idx]
def qcolor(self, key: str) -> QColor: def qcolor(self, key: str) -> QColor:
"""Get a color defined in _vars.scss as a QColor.""" """Get a color defined in _vars.scss as a QColor."""

View file

@ -805,12 +805,17 @@ def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
# Menus # Menus
###################################################################### ######################################################################
# This code will be removed in the future, please don't rely on it.
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"] MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
class MenuList: class MenuList:
def __init__(self) -> None: def __init__(self) -> None:
traceback.print_stack(file=sys.stdout)
print(
"MenuList will be removed; please copy it into your add-on's code if you need it."
)
self.children: List[MenuListChild] = [] self.children: List[MenuListChild] = []
def addItem(self, title: str, func: Callable) -> MenuItem: def addItem(self, title: str, func: Callable) -> MenuItem:

1
qt/icons/README.md Normal file
View file

@ -0,0 +1 @@
Source files used to produce some of the svg/png files.

BIN
qt/icons/sidebar.afdesign Normal file

Binary file not shown.

View file

@ -32,4 +32,6 @@ for line in open(input_scss):
with open(output_py, "w") as buf: with open(output_py, "w") as buf:
buf.write("# this file is auto-generated from _vars.scss\n") buf.write("# this file is auto-generated from _vars.scss\n")
buf.write("colors = " + json.dumps(colors)) for color, (day, night) in colors.items():
color = color.replace("-", "_").upper()
buf.write(f"{color} = (\"{day}\", \"{night}\")\n")

View file

@ -304,8 +304,8 @@ hooks = [
name="browser_will_build_tree", name="browser_will_build_tree",
args=[ args=[
"handled: bool", "handled: bool",
"tree: aqt.browser.SidebarItem", "tree: aqt.sidebar.SidebarItem",
"stage: aqt.browser.SidebarStage", "stage: aqt.sidebar.SidebarStage",
"browser: aqt.browser.Browser", "browser: aqt.browser.Browser",
], ],
return_type="bool", return_type="bool",
@ -316,7 +316,7 @@ hooks = [
'stage' is an enum describing the different construction stages of 'stage' is an enum describing the different construction stages of
the sidebar tree at which you can interject your changes. the sidebar tree at which you can interject your changes.
The different values can be inspected by looking at The different values can be inspected by looking at
aqt.browser.SidebarStage. aqt.sidebar.SidebarStage.
If you want Anki to proceed with the construction of the tree stage If you want Anki to proceed with the construction of the tree stage
in question after your have performed your changes or additions, in question after your have performed your changes or additions,

View file

@ -821,6 +821,7 @@ message SearchTerm {
Flag flag = 11; Flag flag = 11;
CardState card_state = 12; CardState card_state = 12;
IdList nids = 13; IdList nids = 13;
uint32 edited_in_days = 14;
} }
} }
@ -1222,8 +1223,10 @@ message ConfigBool {
COLLAPSE_TAGS = 2; COLLAPSE_TAGS = 2;
COLLAPSE_NOTETYPES = 3; COLLAPSE_NOTETYPES = 3;
COLLAPSE_DECKS = 4; COLLAPSE_DECKS = 4;
COLLAPSE_FAVORITES = 5; COLLAPSE_SAVED_SEARCHES = 5;
COLLAPSE_COMMON = 6; COLLAPSE_RECENT = 6;
COLLAPSE_CARD_STATE = 7;
COLLAPSE_FLAGS = 8;
} }
Key key = 1; Key key = 1;
} }

View file

@ -330,6 +330,7 @@ impl From<pb::SearchTerm> for Node<'_> {
operator: "<=".to_string(), operator: "<=".to_string(),
kind: PropertyKind::Due(i), kind: PropertyKind::Due(i),
}), }),
Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)),
Filter::CardState(state) => Node::Search(SearchNode::State( Filter::CardState(state) => Node::Search(SearchNode::State(
pb::search_term::CardState::from_i32(state) pb::search_term::CardState::from_i32(state)
.unwrap_or_default() .unwrap_or_default()

View file

@ -42,10 +42,12 @@ pub(crate) enum ConfigKey {
BrowserSortKind, BrowserSortKind,
BrowserSortReverse, BrowserSortReverse,
CardCountsSeparateInactive, CardCountsSeparateInactive,
CollapseCommon, CollapseCardState,
CollapseDecks, CollapseDecks,
CollapseFavorites, CollapseFlags,
CollapseNotetypes, CollapseNotetypes,
CollapseRecent,
CollapseSavedSearches,
CollapseTags, CollapseTags,
CreationOffset, CreationOffset,
CurrentDeckID, CurrentDeckID,
@ -79,9 +81,11 @@ impl From<ConfigKey> for &'static str {
ConfigKey::BrowserSortKind => "sortType", ConfigKey::BrowserSortKind => "sortType",
ConfigKey::BrowserSortReverse => "sortBackwards", ConfigKey::BrowserSortReverse => "sortBackwards",
ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive", ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive",
ConfigKey::CollapseCommon => "collapseCommon", ConfigKey::CollapseRecent => "collapseRecent",
ConfigKey::CollapseCardState => "collapseCardState",
ConfigKey::CollapseFlags => "collapseFlags",
ConfigKey::CollapseDecks => "collapseDecks", ConfigKey::CollapseDecks => "collapseDecks",
ConfigKey::CollapseFavorites => "collapseFavorites", ConfigKey::CollapseSavedSearches => "collapseSavedSearches",
ConfigKey::CollapseNotetypes => "collapseNotetypes", ConfigKey::CollapseNotetypes => "collapseNotetypes",
ConfigKey::CollapseTags => "collapseTags", ConfigKey::CollapseTags => "collapseTags",
ConfigKey::CreationOffset => "creationOffset", ConfigKey::CreationOffset => "creationOffset",
@ -109,12 +113,14 @@ impl From<BoolKey> for ConfigKey {
fn from(key: BoolKey) -> Self { fn from(key: BoolKey) -> Self {
match key { match key {
BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse, BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse,
BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides, BoolKey::CollapseCardState => ConfigKey::CollapseCardState,
BoolKey::CollapseTags => ConfigKey::CollapseTags,
BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes,
BoolKey::CollapseDecks => ConfigKey::CollapseDecks, BoolKey::CollapseDecks => ConfigKey::CollapseDecks,
BoolKey::CollapseFavorites => ConfigKey::CollapseFavorites, BoolKey::CollapseFlags => ConfigKey::CollapseFlags,
BoolKey::CollapseCommon => ConfigKey::CollapseCommon, BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes,
BoolKey::CollapseRecent => ConfigKey::CollapseRecent,
BoolKey::CollapseSavedSearches => ConfigKey::CollapseSavedSearches,
BoolKey::CollapseTags => ConfigKey::CollapseTags,
BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides,
} }
} }
} }

View file

@ -16,10 +16,16 @@
--highlight-bg: #77ccff; --highlight-bg: #77ccff;
--highlight-fg: black; --highlight-fg: black;
--disabled: #777; --disabled: #777;
--flag1-fg: #c35617;
--flag2-fg: #ffb347;
--flag3-fg: #0a0;
--flag4-fg: #77ccff;
--flag1-bg: #ffaaaa; --flag1-bg: #ffaaaa;
--flag2-bg: #ffb347; --flag2-bg: #ffb347;
--flag3-bg: #82e0aa; --flag3-bg: #82e0aa;
--flag4-bg: #85c1e9; --flag4-bg: #85c1e9;
--buried-fg: #aaaa33;
--suspended-fg: #dd0;
--suspended-bg: #ffffb2; --suspended-bg: #ffffb2;
--marked-bg: #cce; --marked-bg: #cce;
--tooltip-bg: #fcfcfc; --tooltip-bg: #fcfcfc;
@ -40,10 +46,16 @@
--highlight-bg: #77ccff; --highlight-bg: #77ccff;
--highlight-fg: white; --highlight-fg: white;
--disabled: #777; --disabled: #777;
--flag1-fg: #ffaaaa;
--flag2-fg: #ffb347;
--flag3-fg: #82e0aa;
--flag4-fg: #85c1e9;
--flag1-bg: #aa5555; --flag1-bg: #aa5555;
--flag2-bg: #aa6337; --flag2-bg: #aa6337;
--flag3-bg: #33a055; --flag3-bg: #33a055;
--flag4-bg: #3581a9; --flag4-bg: #3581a9;
--buried-fg: #777733;
--suspended-fg: #ffffb2;
--suspended-bg: #aaaa33; --suspended-bg: #aaaa33;
--marked-bg: #77c; --marked-bg: #77c;
--tooltip-bg: #272727; --tooltip-bg: #272727;