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-addon = Add-on
browsing-again-today = Again Today
browsing-edited-today = Edited Today
browsing-all-card-types = All Card Types
browsing-all-fields = All Fields
browsing-answer = Answer
@ -34,7 +35,6 @@ browsing-ease = Ease
browsing-end = End
browsing-enter-tags-to-add = Enter tags to add:
browsing-enter-tags-to-delete = Enter tags to delete:
browsing-filter = Filter...
browsing-filtered = (filtered)
browsing-find = <b>Find</b>:
browsing-find-and-replace = Find and Replace
@ -127,3 +127,8 @@ browsing-sidebar-tags = Tags
browsing-sidebar-notetypes = Note Types
browsing-sidebar-saved-searches = Saved Searches
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:
"""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}")

View File

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

View File

@ -33,8 +33,6 @@ from aqt.theme import theme_manager
from aqt.utils import (
TR,
HelpPage,
MenuList,
SubMenu,
askUser,
disable_help_button,
getTag,
@ -63,10 +61,6 @@ from aqt.utils import (
)
from aqt.webview import AnkiWebView
# legacy add-on support
# pylint: disable=unused-import
from aqt.sidebar import SidebarItem, SidebarStage # isort: skip
@dataclass
class FindDupesDialog:
@ -486,7 +480,6 @@ class Browser(QMainWindow):
# pylint: disable=unnecessary-lambda
# actions
f = self.form
qconnect(f.filter.clicked, self.onFilterButton)
# edit
qconnect(f.actionUndo.triggered, self.mw.onUndo)
qconnect(f.actionInvertSelection.triggered, self.invertSelection)
@ -978,34 +971,15 @@ QTableView {{ gridline-color: {grid} }}
self.showSidebar()
self.sidebar.searchBar.setFocus()
# legacy
def maybeRefreshSidebar(self) -> None:
self.sidebar.refresh()
def toggle_sidebar(self) -> None:
want_visible = not self.sidebarDockWidget.isVisible()
self.sidebarDockWidget.setVisible(want_visible)
if want_visible:
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:
"""Modify the current search string based on modified keys, then refresh."""
try:
@ -1030,84 +1004,6 @@ QTableView {{ gridline-color: {grid} }}
def setFilter(self, *terms: str) -> None:
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
######################################################################

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

View File

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

View File

@ -5,7 +5,7 @@
from __future__ import annotations
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
import aqt
@ -13,11 +13,12 @@ from anki.collection import ConfigBoolKey, SearchTerm
from anki.decks import DeckTreeNode
from anki.errors import DeckRenameError, InvalidInput
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.models import Models
from aqt.qt import *
from aqt.theme import theme_manager
from aqt.theme import ColoredIcon, theme_manager
from aqt.utils import (
TR,
askUser,
@ -30,39 +31,51 @@ from aqt.utils import (
class SidebarItemType(Enum):
ROOT = 0
COLLECTION = 1
CURRENT_DECK = 2
SAVED_SEARCH = 3
FILTER = 3 # legacy alias for SAVED_SEARCH
DECK = 4
NOTETYPE = 5
TAG = 6
CUSTOM = 7
TEMPLATE = 8
SAVED_SEARCH_ROOT = 9
DECK_ROOT = 10
NOTETYPE_ROOT = 11
TAG_ROOT = 12
ROOT = auto()
SAVED_SEARCH_ROOT = auto()
SAVED_SEARCH = auto()
RECENT_ROOT = auto()
RECENT = auto()
CARD_STATE_ROOT = auto()
CARD_STATE = auto()
FLAG_ROOT = auto()
FLAG = auto()
DECK_ROOT = auto()
DECK = auto()
NOTETYPE_ROOT = auto()
NOTETYPE = auto()
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):
ROOT = 0
STANDARD = 1
FAVORITES = 2
DECKS = 3
MODELS = 4
TAGS = 5
ROOT = auto()
SAVED_SEARCHES = auto()
RECENT = auto()
SCHEDULING = auto()
FLAGS = auto()
DECKS = auto()
NOTETYPES = auto()
TAGS = auto()
class SidebarItem:
def __init__(
self,
name: str,
icon: str,
onClick: Callable[[], None] = None,
onExpanded: Callable[[bool], None] = None,
icon: Union[str, ColoredIcon],
on_click: Callable[[], None] = None,
on_expanded: Callable[[bool], None] = None,
expanded: bool = False,
item_type: SidebarItemType = SidebarItemType.CUSTOM,
id: int = 0,
@ -75,39 +88,47 @@ class SidebarItem:
self.icon = icon
self.item_type = item_type
self.id = id
self.onClick = onClick
self.onExpanded = onExpanded
self.expanded = expanded
self.on_click = on_click
self.on_expanded = on_expanded
self.children: List["SidebarItem"] = []
self.parentItem: Optional["SidebarItem"] = 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_child = False
def addChild(self, cb: "SidebarItem") -> None:
def add_child(self, cb: "SidebarItem") -> None:
self.children.append(cb)
cb.parentItem = self
cb._parent_item = self
def rowForChild(self, child: "SidebarItem") -> Optional[int]:
try:
return self.children.index(child)
except ValueError:
return None
def add_simple(
self,
name: Union[str, TR.V],
icon: Union[str, ColoredIcon],
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:
if not searching:
return self.expanded
return self._is_expanded
else:
if self._search_matches_child:
return True
# if search matches top level, expand children one level
return self._search_matches_self and self.item_type in (
SidebarItemType.SAVED_SEARCH_ROOT,
SidebarItemType.DECK_ROOT,
SidebarItemType.NOTETYPE_ROOT,
SidebarItemType.TAG_ROOT,
)
return self._search_matches_self and self.item_type.is_section_root()
def is_highlighted(self) -> bool:
return self._search_matches_self
@ -130,7 +151,7 @@ class SidebarModel(QAbstractItemModel):
def _cache_rows(self, node: SidebarItem) -> None:
"Cache index of children in parent."
for row, item in enumerate(node.children):
item.row_in_parent = row
item._row_in_parent = row
self._cache_rows(item)
def item_for_index(self, idx: QModelIndex) -> SidebarItem:
@ -172,12 +193,12 @@ class SidebarModel(QAbstractItemModel):
return QModelIndex()
childItem: SidebarItem = child.internalPointer()
parentItem = childItem.parentItem
parentItem = childItem._parent_item
if parentItem is None or parentItem == self.root:
return QModelIndex()
row = parentItem.row_in_parent
row = parentItem._row_in_parent
return self.createIndex(row, 0, parentItem)
@ -216,30 +237,6 @@ class SidebarModel(QAbstractItemModel):
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):
def __init__(self, sidebar: SidebarTreeView) -> None:
@ -308,8 +305,8 @@ class SidebarTreeView(QTreeView):
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragDropOverwriteMode(False)
qconnect(self.expanded, self.onExpansion)
qconnect(self.collapsed, self.onCollapse)
qconnect(self.expanded, self._on_expansion)
qconnect(self.collapsed, self._on_collapse)
# match window background color
bgcolor = QPalette().window().color().name()
@ -334,7 +331,7 @@ class SidebarTreeView(QTreeView):
if self.current_search:
self.search_for(self.current_search)
else:
expand_where_necessary(model, self)
self._expand_where_necessary(model)
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
self.collapseAll()
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(
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
@ -369,6 +385,19 @@ class SidebarTreeView(QTreeView):
if self.handle_drag_drop(source_items, target_item):
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:
if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
return self._handle_drag_drop_decks(sources, target)
@ -433,74 +462,70 @@ class SidebarTreeView(QTreeView):
self.browser.editor.saveNow(on_save)
return True
def onClickCurrent(self) -> None:
def _on_click_current(self) -> None:
idx = self.currentIndex()
if item := self.model().item_for_index(idx):
if item.onClick:
item.onClick()
if item.on_click:
item.on_click()
def mouseReleaseEvent(self, event: QMouseEvent) -> 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:
def _on_expansion(self, idx: QModelIndex) -> None:
if self.current_search:
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:
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)
if item and item.expanded != expanded:
item.expanded = expanded
if item.onExpanded:
item.onExpanded(expanded)
if item and item._is_expanded != expanded:
item._is_expanded = expanded
if item.on_expanded:
item.on_expanded(expanded)
# Tree building
###########################
def _root_tree(self) -> SidebarItem:
root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
root: Optional[SidebarItem] = None
handled = gui_hooks.browser_will_build_tree(
False, root, SidebarStage.ROOT, self
)
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,
),
):
for stage in SidebarStage:
if stage == SidebarStage.ROOT:
root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
handled = gui_hooks.browser_will_build_tree(False, root, stage, self)
if not handled and builder:
builder(root)
if not handled:
self._build_stage(root, stage)
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(
self,
*,
root: SidebarItem,
name: TR.V,
icon: str,
icon: Union[str, ColoredIcon],
collapse_key: ConfigBoolKey.V,
type: Optional[SidebarItemType] = None,
) -> SidebarItem:
@ -510,29 +535,19 @@ class SidebarTreeView(QTreeView):
top = SidebarItem(
tr(name),
icon,
onExpanded=update,
on_expanded=update,
expanded=not self.col.get_config_bool(collapse_key),
item_type=type,
)
root.addChild(top)
root.add_child(top)
return top
def _commonly_used_tree(self, root: SidebarItem) -> None:
item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg",
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 _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
return lambda: self.browser.update_search(self.col.build_search_string(*terms))
# Tree: Saved Searches
###########################
def _saved_searches_tree(self, root: SidebarItem) -> None:
icon = ":/icons/heart.svg"
@ -542,23 +557,187 @@ class SidebarTreeView(QTreeView):
root=root,
name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES,
icon=icon,
collapse_key=ConfigBoolKey.COLLAPSE_FAVORITES,
collapse_key=ConfigBoolKey.COLLAPSE_SAVED_SEARCHES,
type=SidebarItemType.SAVED_SEARCH_ROOT,
)
def on_click() -> None:
self.show_context_menu(root, None)
root.onClick = on_click
root.on_click = on_click
for name, filt in sorted(saved.items()):
item = SidebarItem(
name,
icon,
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:
icon = ":/icons/tag.svg"
@ -583,7 +762,7 @@ class SidebarTreeView(QTreeView):
item_type=SidebarItemType.TAG,
full_name=head + node.name,
)
root.addChild(item)
root.add_child(item)
newhead = head + node.name + "::"
render(item, node.children, newhead)
@ -597,6 +776,9 @@ class SidebarTreeView(QTreeView):
)
render(root, tree.children)
# Tree: Decks
###########################
def _deck_tree(self, root: SidebarItem) -> None:
icon = ":/icons/deck.svg"
@ -619,7 +801,7 @@ class SidebarTreeView(QTreeView):
id=node.deck_id,
full_name=head + node.name,
)
root.addChild(item)
root.add_child(item)
newhead = head + node.name + "::"
render(item, node.children, newhead)
@ -633,6 +815,9 @@ class SidebarTreeView(QTreeView):
)
render(root, tree.children)
# Tree: Notetypes
###########################
def _notetype_tree(self, root: SidebarItem) -> None:
icon = ":/icons/notetype.svg"
root = self._section_root(
@ -659,15 +844,12 @@ class SidebarTreeView(QTreeView):
self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c)
),
item_type=SidebarItemType.TEMPLATE,
item_type=SidebarItemType.NOTETYPE_TEMPLATE,
full_name=nt["name"] + "::" + tmpl["name"],
)
item.addChild(child)
item.add_child(child)
root.addChild(item)
def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
return lambda: self.browser.update_search(self.col.build_search_string(*terms))
root.add_child(item)
# Context menu actions
###########################
@ -735,7 +917,7 @@ class SidebarTreeView(QTreeView):
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)
old_name = deck["name"]
new_name = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=old_name)
@ -751,10 +933,10 @@ class SidebarTreeView(QTreeView):
self.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))
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None:
def _remove_tag(self, item: SidebarItem) -> None:
old_name = item.full_name
def do_remove() -> None:
@ -771,10 +953,10 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset()
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))
def _rename_tag(self, item: "aqt.browser.SidebarItem") -> None:
def _rename_tag(self, item: SidebarItem) -> None:
old_name = item.full_name
new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name)
if new_name == old_name or not new_name:
@ -799,10 +981,10 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset()
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))
def _delete_deck(self, item: "aqt.browser.SidebarItem") -> None:
def _delete_deck(self, item: SidebarItem) -> None:
did = item.id
if self.mw.deckBrowser.ask_delete_deck(did):
@ -820,7 +1002,7 @@ class SidebarTreeView(QTreeView):
self.browser.model.beginReset()
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
if not askUser(tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=name)):
return
@ -829,7 +1011,7 @@ class SidebarTreeView(QTreeView):
self.col.set_config("savedFilters", conf)
self.refresh()
def rename_saved_search(self, item: "aqt.browser.SidebarItem") -> None:
def rename_saved_search(self, item: SidebarItem) -> None:
old = item.name
conf = self.col.get_config("savedFilters")
try:
@ -860,7 +1042,7 @@ class SidebarTreeView(QTreeView):
self.col.set_config("savedFilters", conf)
self.refresh()
def manage_notetype(self, item: "aqt.browser.SidebarItem") -> None:
def manage_notetype(self, item: SidebarItem) -> None:
Models(
self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id
)

View File

@ -2,14 +2,32 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import platform
from typing import Dict, Optional
from dataclasses import dataclass
from typing import Dict, Optional, Tuple, Union
from anki.utils import isMac
from aqt import QApplication, gui_hooks, isWin
from aqt.colors import colors
from aqt import QApplication, colors, gui_hooks, isWin
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:
@ -43,22 +61,39 @@ class ThemeManager:
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."
if self.night_mode:
cache = self._icon_cache_light
else:
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:
return icon
icon = QIcon(path)
if self.night_mode:
img = icon.pixmap(self._icon_size, self._icon_size).toImage()
img.invertPixels()
icon = QIcon(QPixmap(img))
if isinstance(path, str):
# default black/white
icon = QIcon(path)
if self.night_mode:
img = icon.pixmap(self._icon_size, self._icon_size).toImage()
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)
@ -94,10 +129,10 @@ class ThemeManager:
Returns the color as a string hex code or color name."""
idx = 1 if self.night_mode else 0
c = colors.get(key)
if c is None:
raise Exception("no such color:", key)
return c[idx]
key = key.replace("-", "_").upper()
return getattr(colors, key)[idx]
def qcolor(self, key: str) -> 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
######################################################################
# This code will be removed in the future, please don't rely on it.
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
class MenuList:
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] = []
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:
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",
args=[
"handled: bool",
"tree: aqt.browser.SidebarItem",
"stage: aqt.browser.SidebarStage",
"tree: aqt.sidebar.SidebarItem",
"stage: aqt.sidebar.SidebarStage",
"browser: aqt.browser.Browser",
],
return_type="bool",
@ -316,7 +316,7 @@ hooks = [
'stage' is an enum describing the different construction stages of
the sidebar tree at which you can interject your changes.
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
in question after your have performed your changes or additions,

View File

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

View File

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

View File

@ -42,10 +42,12 @@ pub(crate) enum ConfigKey {
BrowserSortKind,
BrowserSortReverse,
CardCountsSeparateInactive,
CollapseCommon,
CollapseCardState,
CollapseDecks,
CollapseFavorites,
CollapseFlags,
CollapseNotetypes,
CollapseRecent,
CollapseSavedSearches,
CollapseTags,
CreationOffset,
CurrentDeckID,
@ -79,9 +81,11 @@ impl From<ConfigKey> for &'static str {
ConfigKey::BrowserSortKind => "sortType",
ConfigKey::BrowserSortReverse => "sortBackwards",
ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive",
ConfigKey::CollapseCommon => "collapseCommon",
ConfigKey::CollapseRecent => "collapseRecent",
ConfigKey::CollapseCardState => "collapseCardState",
ConfigKey::CollapseFlags => "collapseFlags",
ConfigKey::CollapseDecks => "collapseDecks",
ConfigKey::CollapseFavorites => "collapseFavorites",
ConfigKey::CollapseSavedSearches => "collapseSavedSearches",
ConfigKey::CollapseNotetypes => "collapseNotetypes",
ConfigKey::CollapseTags => "collapseTags",
ConfigKey::CreationOffset => "creationOffset",
@ -109,12 +113,14 @@ impl From<BoolKey> for ConfigKey {
fn from(key: BoolKey) -> Self {
match key {
BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse,
BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides,
BoolKey::CollapseTags => ConfigKey::CollapseTags,
BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes,
BoolKey::CollapseCardState => ConfigKey::CollapseCardState,
BoolKey::CollapseDecks => ConfigKey::CollapseDecks,
BoolKey::CollapseFavorites => ConfigKey::CollapseFavorites,
BoolKey::CollapseCommon => ConfigKey::CollapseCommon,
BoolKey::CollapseFlags => ConfigKey::CollapseFlags,
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-fg: black;
--disabled: #777;
--flag1-fg: #c35617;
--flag2-fg: #ffb347;
--flag3-fg: #0a0;
--flag4-fg: #77ccff;
--flag1-bg: #ffaaaa;
--flag2-bg: #ffb347;
--flag3-bg: #82e0aa;
--flag4-bg: #85c1e9;
--buried-fg: #aaaa33;
--suspended-fg: #dd0;
--suspended-bg: #ffffb2;
--marked-bg: #cce;
--tooltip-bg: #fcfcfc;
@ -40,10 +46,16 @@
--highlight-bg: #77ccff;
--highlight-fg: white;
--disabled: #777;
--flag1-fg: #ffaaaa;
--flag2-fg: #ffb347;
--flag3-fg: #82e0aa;
--flag4-fg: #85c1e9;
--flag1-bg: #aa5555;
--flag2-bg: #aa6337;
--flag3-bg: #33a055;
--flag4-bg: #3581a9;
--buried-fg: #777733;
--suspended-fg: #ffffb2;
--suspended-bg: #aaaa33;
--marked-bg: #77c;
--tooltip-bg: #272727;