mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Refactor sidebar.py into browser folder
This commit is contained in:
parent
dac990e4c2
commit
7ee40e3dce
9 changed files with 397 additions and 323 deletions
|
@ -5,6 +5,16 @@ from __future__ import annotations
|
||||||
|
|
||||||
from .browser import Browser
|
from .browser import Browser
|
||||||
from .dialogs import CardInfoDialog, ChangeModel, FindDupesDialog
|
from .dialogs import CardInfoDialog, ChangeModel, FindDupesDialog
|
||||||
|
from .sidebar import (
|
||||||
|
SidebarItem,
|
||||||
|
SidebarItemType,
|
||||||
|
SidebarModel,
|
||||||
|
SidebarSearchBar,
|
||||||
|
SidebarStage,
|
||||||
|
SidebarTool,
|
||||||
|
SidebarToolbar,
|
||||||
|
SidebarTreeView,
|
||||||
|
)
|
||||||
from .table import (
|
from .table import (
|
||||||
CardState,
|
CardState,
|
||||||
Cell,
|
Cell,
|
||||||
|
|
|
@ -19,6 +19,7 @@ from anki.tags import MARKED_TAG
|
||||||
from anki.utils import ids2str, isMac
|
from anki.utils import ids2str, isMac
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.browser.dialogs import CardInfoDialog, ChangeModel, FindDupesDialog
|
from aqt.browser.dialogs import CardInfoDialog, ChangeModel, FindDupesDialog
|
||||||
|
from aqt.browser.sidebar import SidebarTreeView
|
||||||
from aqt.browser.table import Table
|
from aqt.browser.table import Table
|
||||||
from aqt.editor import Editor
|
from aqt.editor import Editor
|
||||||
from aqt.exporting import ExportDialog
|
from aqt.exporting import ExportDialog
|
||||||
|
@ -42,7 +43,6 @@ from aqt.operations.tag import (
|
||||||
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
||||||
from aqt.previewer import Previewer
|
from aqt.previewer import Previewer
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.sidebar import SidebarTreeView
|
|
||||||
from aqt.switch import Switch
|
from aqt.switch import Switch
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
HelpPage,
|
HelpPage,
|
||||||
|
|
22
qt/aqt/browser/sidebar/__init__.py
Normal file
22
qt/aqt/browser/sidebar/__init__.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import aqt
|
||||||
|
from anki.utils import isMac
|
||||||
|
from aqt.theme import theme_manager
|
||||||
|
|
||||||
|
|
||||||
|
def _want_right_border() -> bool:
|
||||||
|
return not isMac or theme_manager.night_mode
|
||||||
|
|
||||||
|
|
||||||
|
from .item import SidebarItem, SidebarItemType
|
||||||
|
from .model import SidebarModel
|
||||||
|
from .searchbar import SidebarSearchBar
|
||||||
|
from .toolbar import SidebarTool, SidebarToolbar
|
||||||
|
from .tree import SidebarStage, SidebarTreeView
|
||||||
|
|
||||||
|
# alias for the legacy pathname
|
||||||
|
sys.modules["aqt.sidebar"] = sys.modules["aqt.browser.sidebar"]
|
||||||
|
aqt.sidebar = sys.modules["aqt.browser.sidebar"] # type: ignore
|
145
qt/aqt/browser/sidebar/item.py
Normal file
145
qt/aqt/browser/sidebar/item.py
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Callable, Iterable, List, Optional, Union
|
||||||
|
|
||||||
|
from anki.collection import SearchNode
|
||||||
|
from aqt.theme import ColoredIcon
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarItemType(Enum):
|
||||||
|
ROOT = auto()
|
||||||
|
SAVED_SEARCH_ROOT = auto()
|
||||||
|
SAVED_SEARCH = auto()
|
||||||
|
TODAY_ROOT = auto()
|
||||||
|
TODAY = auto()
|
||||||
|
FLAG_ROOT = auto()
|
||||||
|
FLAG = auto()
|
||||||
|
CARD_STATE_ROOT = auto()
|
||||||
|
CARD_STATE = auto()
|
||||||
|
DECK_ROOT = auto()
|
||||||
|
DECK_CURRENT = auto()
|
||||||
|
DECK = auto()
|
||||||
|
NOTETYPE_ROOT = auto()
|
||||||
|
NOTETYPE = auto()
|
||||||
|
NOTETYPE_TEMPLATE = auto()
|
||||||
|
TAG_ROOT = auto()
|
||||||
|
TAG_NONE = 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()
|
||||||
|
|
||||||
|
def is_editable(self) -> bool:
|
||||||
|
return self in (
|
||||||
|
SidebarItemType.SAVED_SEARCH,
|
||||||
|
SidebarItemType.DECK,
|
||||||
|
SidebarItemType.TAG,
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_deletable(self) -> bool:
|
||||||
|
return self in (
|
||||||
|
SidebarItemType.SAVED_SEARCH,
|
||||||
|
SidebarItemType.DECK,
|
||||||
|
SidebarItemType.TAG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarItem:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
icon: Union[str, ColoredIcon],
|
||||||
|
search_node: Optional[SearchNode] = None,
|
||||||
|
on_expanded: Callable[[bool], None] = None,
|
||||||
|
expanded: bool = False,
|
||||||
|
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
||||||
|
id: int = 0,
|
||||||
|
name_prefix: str = "",
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.name_prefix = name_prefix
|
||||||
|
self.full_name = name_prefix + name
|
||||||
|
self.icon = icon
|
||||||
|
self.item_type = item_type
|
||||||
|
self.id = id
|
||||||
|
self.search_node = search_node
|
||||||
|
self.on_expanded = on_expanded
|
||||||
|
self.children: List["SidebarItem"] = []
|
||||||
|
self.tooltip: Optional[str] = None
|
||||||
|
self._parent_item: Optional["SidebarItem"] = None
|
||||||
|
self._expanded = expanded
|
||||||
|
self._row_in_parent: Optional[int] = None
|
||||||
|
self._search_matches_self = False
|
||||||
|
self._search_matches_child = False
|
||||||
|
|
||||||
|
def add_child(self, cb: "SidebarItem") -> None:
|
||||||
|
self.children.append(cb)
|
||||||
|
cb._parent_item = self
|
||||||
|
|
||||||
|
def add_simple(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
icon: Union[str, ColoredIcon],
|
||||||
|
type: SidebarItemType,
|
||||||
|
search_node: Optional[SearchNode],
|
||||||
|
) -> SidebarItem:
|
||||||
|
"Add child sidebar item, and return it."
|
||||||
|
item = SidebarItem(
|
||||||
|
name=name,
|
||||||
|
icon=icon,
|
||||||
|
search_node=search_node,
|
||||||
|
item_type=type,
|
||||||
|
)
|
||||||
|
self.add_child(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expanded(self) -> bool:
|
||||||
|
return self._expanded
|
||||||
|
|
||||||
|
@expanded.setter
|
||||||
|
def expanded(self, expanded: bool) -> None:
|
||||||
|
if self.expanded != expanded:
|
||||||
|
self._expanded = expanded
|
||||||
|
if self.on_expanded:
|
||||||
|
self.on_expanded(expanded)
|
||||||
|
|
||||||
|
def show_expanded(self, searching: bool) -> bool:
|
||||||
|
if not searching:
|
||||||
|
return self.expanded
|
||||||
|
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.is_section_root()
|
||||||
|
|
||||||
|
def is_highlighted(self) -> bool:
|
||||||
|
return self._search_matches_self
|
||||||
|
|
||||||
|
def search(self, lowered_text: str) -> bool:
|
||||||
|
"True if we or child matched."
|
||||||
|
self._search_matches_self = lowered_text in self.name.lower()
|
||||||
|
self._search_matches_child = any(
|
||||||
|
[child.search(lowered_text) for child in self.children]
|
||||||
|
)
|
||||||
|
return self._search_matches_self or self._search_matches_child
|
||||||
|
|
||||||
|
def has_same_id(self, other: SidebarItem) -> bool:
|
||||||
|
"True if `other` is same type, with same id/name."
|
||||||
|
if other.item_type == self.item_type:
|
||||||
|
if self.item_type == SidebarItemType.TAG:
|
||||||
|
return self.full_name == other.full_name
|
||||||
|
elif self.item_type == SidebarItemType.SAVED_SEARCH:
|
||||||
|
return self.name == other.name
|
||||||
|
else:
|
||||||
|
return other.id == self.id
|
||||||
|
|
||||||
|
return False
|
110
qt/aqt/browser/sidebar/model.py
Normal file
110
qt/aqt/browser/sidebar/model.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import aqt
|
||||||
|
from aqt.browser.sidebar.item import SidebarItem
|
||||||
|
from aqt.qt import *
|
||||||
|
from aqt.theme import theme_manager
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarModel(QAbstractItemModel):
|
||||||
|
def __init__(
|
||||||
|
self, sidebar: aqt.browser.sidebar.SidebarTreeView, root: SidebarItem
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.sidebar = sidebar
|
||||||
|
self.root = root
|
||||||
|
self._cache_rows(root)
|
||||||
|
|
||||||
|
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
|
||||||
|
self._cache_rows(item)
|
||||||
|
|
||||||
|
def item_for_index(self, idx: QModelIndex) -> SidebarItem:
|
||||||
|
return idx.internalPointer()
|
||||||
|
|
||||||
|
def index_for_item(self, item: SidebarItem) -> QModelIndex:
|
||||||
|
return self.createIndex(item._row_in_parent, 0, item)
|
||||||
|
|
||||||
|
def search(self, text: str) -> bool:
|
||||||
|
return self.root.search(text.lower())
|
||||||
|
|
||||||
|
# Qt API
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
if not parent.isValid():
|
||||||
|
return len(self.root.children)
|
||||||
|
else:
|
||||||
|
item: SidebarItem = parent.internalPointer()
|
||||||
|
return len(item.children)
|
||||||
|
|
||||||
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def index(
|
||||||
|
self, row: int, column: int, parent: QModelIndex = QModelIndex()
|
||||||
|
) -> QModelIndex:
|
||||||
|
if not self.hasIndex(row, column, parent):
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
parentItem: SidebarItem
|
||||||
|
if not parent.isValid():
|
||||||
|
parentItem = self.root
|
||||||
|
else:
|
||||||
|
parentItem = parent.internalPointer()
|
||||||
|
|
||||||
|
item = parentItem.children[row]
|
||||||
|
return self.createIndex(row, column, item)
|
||||||
|
|
||||||
|
def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
|
||||||
|
if not child.isValid():
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
childItem: SidebarItem = child.internalPointer()
|
||||||
|
parentItem = childItem._parent_item
|
||||||
|
|
||||||
|
if parentItem is None or parentItem == self.root:
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
row = parentItem._row_in_parent
|
||||||
|
|
||||||
|
return self.createIndex(row, 0, parentItem)
|
||||||
|
|
||||||
|
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
|
||||||
|
if not index.isValid():
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole):
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
item: SidebarItem = index.internalPointer()
|
||||||
|
|
||||||
|
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||||
|
return QVariant(item.name)
|
||||||
|
if role == Qt.ToolTipRole:
|
||||||
|
return QVariant(item.tooltip)
|
||||||
|
return QVariant(theme_manager.icon_from_resources(item.icon))
|
||||||
|
|
||||||
|
def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool:
|
||||||
|
return self.sidebar._on_rename(index.internalPointer(), text)
|
||||||
|
|
||||||
|
def supportedDropActions(self) -> Qt.DropActions:
|
||||||
|
return cast(Qt.DropActions, Qt.MoveAction)
|
||||||
|
|
||||||
|
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||||
|
if not index.isValid():
|
||||||
|
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
|
||||||
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
||||||
|
item: SidebarItem = index.internalPointer()
|
||||||
|
if item.item_type in self.sidebar.valid_drop_types:
|
||||||
|
flags |= Qt.ItemIsDropEnabled
|
||||||
|
if item.item_type.is_editable():
|
||||||
|
flags |= Qt.ItemIsEditable
|
||||||
|
|
||||||
|
return cast(Qt.ItemFlags, flags)
|
50
qt/aqt/browser/sidebar/searchbar.py
Normal file
50
qt/aqt/browser/sidebar/searchbar.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# 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 aqt
|
||||||
|
from aqt import colors
|
||||||
|
from aqt.browser.sidebar import _want_right_border
|
||||||
|
from aqt.qt import *
|
||||||
|
from aqt.theme import theme_manager
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarSearchBar(QLineEdit):
|
||||||
|
def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None:
|
||||||
|
QLineEdit.__init__(self, sidebar)
|
||||||
|
self.setPlaceholderText(sidebar.col.tr.browsing_sidebar_filter())
|
||||||
|
self.sidebar = sidebar
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.setInterval(600)
|
||||||
|
self.timer.setSingleShot(True)
|
||||||
|
self.setFrame(False)
|
||||||
|
border = theme_manager.color(colors.MEDIUM_BORDER)
|
||||||
|
styles = [
|
||||||
|
"padding: 1px",
|
||||||
|
"padding-left: 3px",
|
||||||
|
f"border-bottom: 1px solid {border}",
|
||||||
|
]
|
||||||
|
if _want_right_border():
|
||||||
|
styles.append(
|
||||||
|
f"border-right: 1px solid {border}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setStyleSheet("QLineEdit { %s }" % ";".join(styles))
|
||||||
|
|
||||||
|
qconnect(self.timer.timeout, self.onSearch)
|
||||||
|
qconnect(self.textChanged, self.onTextChanged)
|
||||||
|
|
||||||
|
def onTextChanged(self, text: str) -> None:
|
||||||
|
if not self.timer.isActive():
|
||||||
|
self.timer.start()
|
||||||
|
|
||||||
|
def onSearch(self) -> None:
|
||||||
|
self.sidebar.search_for(self.text())
|
||||||
|
|
||||||
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||||
|
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
||||||
|
self.sidebar.setFocus()
|
||||||
|
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
||||||
|
self.onSearch()
|
||||||
|
else:
|
||||||
|
QLineEdit.keyPressEvent(self, evt)
|
51
qt/aqt/browser/sidebar/toolbar.py
Normal file
51
qt/aqt/browser/sidebar/toolbar.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Callable, Tuple
|
||||||
|
|
||||||
|
import aqt
|
||||||
|
from aqt.qt import *
|
||||||
|
from aqt.theme import theme_manager
|
||||||
|
from aqt.utils import tr
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarTool(Enum):
|
||||||
|
SELECT = auto()
|
||||||
|
SEARCH = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarToolbar(QToolBar):
|
||||||
|
_tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = (
|
||||||
|
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
|
||||||
|
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.sidebar = sidebar
|
||||||
|
self._action_group = QActionGroup(self)
|
||||||
|
qconnect(self._action_group.triggered, self._on_action_group_triggered)
|
||||||
|
self._setup_tools()
|
||||||
|
self.setIconSize(QSize(16, 16))
|
||||||
|
self.setStyle(QStyleFactory.create("fusion"))
|
||||||
|
|
||||||
|
def _setup_tools(self) -> None:
|
||||||
|
for row, tool in enumerate(self._tools):
|
||||||
|
action = self.addAction(
|
||||||
|
theme_manager.icon_from_resources(tool[1]), tool[2]()
|
||||||
|
)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setShortcut(f"Alt+{row + 1}")
|
||||||
|
self._action_group.addAction(action)
|
||||||
|
saved = self.sidebar.col.get_config("sidebarTool", 0)
|
||||||
|
active = saved if saved < len(self._tools) else 0
|
||||||
|
self._action_group.actions()[active].setChecked(True)
|
||||||
|
self.sidebar.tool = self._tools[active][0]
|
||||||
|
|
||||||
|
def _on_action_group_triggered(self, action: QAction) -> None:
|
||||||
|
index = self._action_group.actions().index(action)
|
||||||
|
self.sidebar.col.set_config("sidebarTool", index)
|
||||||
|
self.sidebar.tool = self._tools[index][0]
|
|
@ -1,6 +1,5 @@
|
||||||
# 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
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
@ -14,6 +13,11 @@ from anki.notes import Note
|
||||||
from anki.tags import TagTreeNode
|
from anki.tags import TagTreeNode
|
||||||
from anki.types import assert_exhaustive
|
from anki.types import assert_exhaustive
|
||||||
from aqt import colors, gui_hooks
|
from aqt import colors, gui_hooks
|
||||||
|
from aqt.browser.sidebar import _want_right_border
|
||||||
|
from aqt.browser.sidebar.item import SidebarItem, SidebarItemType
|
||||||
|
from aqt.browser.sidebar.model import SidebarModel
|
||||||
|
from aqt.browser.sidebar.searchbar import SidebarSearchBar
|
||||||
|
from aqt.browser.sidebar.toolbar import SidebarTool, SidebarToolbar
|
||||||
from aqt.clayout import CardLayout
|
from aqt.clayout import CardLayout
|
||||||
from aqt.models import Models
|
from aqt.models import Models
|
||||||
from aqt.operations.deck import (
|
from aqt.operations.deck import (
|
||||||
|
@ -33,55 +37,6 @@ from aqt.theme import ColoredIcon, theme_manager
|
||||||
from aqt.utils import KeyboardModifiersPressed, askUser, getOnlyText, showWarning, tr
|
from aqt.utils import KeyboardModifiersPressed, askUser, getOnlyText, showWarning, tr
|
||||||
|
|
||||||
|
|
||||||
class SidebarTool(Enum):
|
|
||||||
SELECT = auto()
|
|
||||||
SEARCH = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class SidebarItemType(Enum):
|
|
||||||
ROOT = auto()
|
|
||||||
SAVED_SEARCH_ROOT = auto()
|
|
||||||
SAVED_SEARCH = auto()
|
|
||||||
TODAY_ROOT = auto()
|
|
||||||
TODAY = auto()
|
|
||||||
FLAG_ROOT = auto()
|
|
||||||
FLAG = auto()
|
|
||||||
CARD_STATE_ROOT = auto()
|
|
||||||
CARD_STATE = auto()
|
|
||||||
DECK_ROOT = auto()
|
|
||||||
DECK_CURRENT = auto()
|
|
||||||
DECK = auto()
|
|
||||||
NOTETYPE_ROOT = auto()
|
|
||||||
NOTETYPE = auto()
|
|
||||||
NOTETYPE_TEMPLATE = auto()
|
|
||||||
TAG_ROOT = auto()
|
|
||||||
TAG_NONE = 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()
|
|
||||||
|
|
||||||
def is_editable(self) -> bool:
|
|
||||||
return self in (
|
|
||||||
SidebarItemType.SAVED_SEARCH,
|
|
||||||
SidebarItemType.DECK,
|
|
||||||
SidebarItemType.TAG,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_deletable(self) -> bool:
|
|
||||||
return self in (
|
|
||||||
SidebarItemType.SAVED_SEARCH,
|
|
||||||
SidebarItemType.DECK,
|
|
||||||
SidebarItemType.TAG,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SidebarStage(Enum):
|
class SidebarStage(Enum):
|
||||||
ROOT = auto()
|
ROOT = auto()
|
||||||
SAVED_SEARCHES = auto()
|
SAVED_SEARCHES = auto()
|
||||||
|
@ -93,275 +48,6 @@ class SidebarStage(Enum):
|
||||||
TAGS = auto()
|
TAGS = auto()
|
||||||
|
|
||||||
|
|
||||||
class SidebarItem:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
icon: Union[str, ColoredIcon],
|
|
||||||
search_node: Optional[SearchNode] = None,
|
|
||||||
on_expanded: Callable[[bool], None] = None,
|
|
||||||
expanded: bool = False,
|
|
||||||
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
|
||||||
id: int = 0,
|
|
||||||
name_prefix: str = "",
|
|
||||||
) -> None:
|
|
||||||
self.name = name
|
|
||||||
self.name_prefix = name_prefix
|
|
||||||
self.full_name = name_prefix + name
|
|
||||||
self.icon = icon
|
|
||||||
self.item_type = item_type
|
|
||||||
self.id = id
|
|
||||||
self.search_node = search_node
|
|
||||||
self.on_expanded = on_expanded
|
|
||||||
self.children: List["SidebarItem"] = []
|
|
||||||
self.tooltip: Optional[str] = None
|
|
||||||
self._parent_item: Optional["SidebarItem"] = None
|
|
||||||
self._expanded = expanded
|
|
||||||
self._row_in_parent: Optional[int] = None
|
|
||||||
self._search_matches_self = False
|
|
||||||
self._search_matches_child = False
|
|
||||||
|
|
||||||
def add_child(self, cb: "SidebarItem") -> None:
|
|
||||||
self.children.append(cb)
|
|
||||||
cb._parent_item = self
|
|
||||||
|
|
||||||
def add_simple(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
icon: Union[str, ColoredIcon],
|
|
||||||
type: SidebarItemType,
|
|
||||||
search_node: Optional[SearchNode],
|
|
||||||
) -> SidebarItem:
|
|
||||||
"Add child sidebar item, and return it."
|
|
||||||
item = SidebarItem(
|
|
||||||
name=name,
|
|
||||||
icon=icon,
|
|
||||||
search_node=search_node,
|
|
||||||
item_type=type,
|
|
||||||
)
|
|
||||||
self.add_child(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expanded(self) -> bool:
|
|
||||||
return self._expanded
|
|
||||||
|
|
||||||
@expanded.setter
|
|
||||||
def expanded(self, expanded: bool) -> None:
|
|
||||||
if self.expanded != expanded:
|
|
||||||
self._expanded = expanded
|
|
||||||
if self.on_expanded:
|
|
||||||
self.on_expanded(expanded)
|
|
||||||
|
|
||||||
def show_expanded(self, searching: bool) -> bool:
|
|
||||||
if not searching:
|
|
||||||
return self.expanded
|
|
||||||
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.is_section_root()
|
|
||||||
|
|
||||||
def is_highlighted(self) -> bool:
|
|
||||||
return self._search_matches_self
|
|
||||||
|
|
||||||
def search(self, lowered_text: str) -> bool:
|
|
||||||
"True if we or child matched."
|
|
||||||
self._search_matches_self = lowered_text in self.name.lower()
|
|
||||||
self._search_matches_child = any(
|
|
||||||
[child.search(lowered_text) for child in self.children]
|
|
||||||
)
|
|
||||||
return self._search_matches_self or self._search_matches_child
|
|
||||||
|
|
||||||
def has_same_id(self, other: SidebarItem) -> bool:
|
|
||||||
"True if `other` is same type, with same id/name."
|
|
||||||
if other.item_type == self.item_type:
|
|
||||||
if self.item_type == SidebarItemType.TAG:
|
|
||||||
return self.full_name == other.full_name
|
|
||||||
elif self.item_type == SidebarItemType.SAVED_SEARCH:
|
|
||||||
return self.name == other.name
|
|
||||||
else:
|
|
||||||
return other.id == self.id
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class SidebarModel(QAbstractItemModel):
|
|
||||||
def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.sidebar = sidebar
|
|
||||||
self.root = root
|
|
||||||
self._cache_rows(root)
|
|
||||||
|
|
||||||
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
|
|
||||||
self._cache_rows(item)
|
|
||||||
|
|
||||||
def item_for_index(self, idx: QModelIndex) -> SidebarItem:
|
|
||||||
return idx.internalPointer()
|
|
||||||
|
|
||||||
def index_for_item(self, item: SidebarItem) -> QModelIndex:
|
|
||||||
return self.createIndex(item._row_in_parent, 0, item)
|
|
||||||
|
|
||||||
def search(self, text: str) -> bool:
|
|
||||||
return self.root.search(text.lower())
|
|
||||||
|
|
||||||
# Qt API
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
||||||
if not parent.isValid():
|
|
||||||
return len(self.root.children)
|
|
||||||
else:
|
|
||||||
item: SidebarItem = parent.internalPointer()
|
|
||||||
return len(item.children)
|
|
||||||
|
|
||||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def index(
|
|
||||||
self, row: int, column: int, parent: QModelIndex = QModelIndex()
|
|
||||||
) -> QModelIndex:
|
|
||||||
if not self.hasIndex(row, column, parent):
|
|
||||||
return QModelIndex()
|
|
||||||
|
|
||||||
parentItem: SidebarItem
|
|
||||||
if not parent.isValid():
|
|
||||||
parentItem = self.root
|
|
||||||
else:
|
|
||||||
parentItem = parent.internalPointer()
|
|
||||||
|
|
||||||
item = parentItem.children[row]
|
|
||||||
return self.createIndex(row, column, item)
|
|
||||||
|
|
||||||
def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
|
|
||||||
if not child.isValid():
|
|
||||||
return QModelIndex()
|
|
||||||
|
|
||||||
childItem: SidebarItem = child.internalPointer()
|
|
||||||
parentItem = childItem._parent_item
|
|
||||||
|
|
||||||
if parentItem is None or parentItem == self.root:
|
|
||||||
return QModelIndex()
|
|
||||||
|
|
||||||
row = parentItem._row_in_parent
|
|
||||||
|
|
||||||
return self.createIndex(row, 0, parentItem)
|
|
||||||
|
|
||||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
|
|
||||||
if not index.isValid():
|
|
||||||
return QVariant()
|
|
||||||
|
|
||||||
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole):
|
|
||||||
return QVariant()
|
|
||||||
|
|
||||||
item: SidebarItem = index.internalPointer()
|
|
||||||
|
|
||||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
|
||||||
return QVariant(item.name)
|
|
||||||
if role == Qt.ToolTipRole:
|
|
||||||
return QVariant(item.tooltip)
|
|
||||||
return QVariant(theme_manager.icon_from_resources(item.icon))
|
|
||||||
|
|
||||||
def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool:
|
|
||||||
return self.sidebar._on_rename(index.internalPointer(), text)
|
|
||||||
|
|
||||||
def supportedDropActions(self) -> Qt.DropActions:
|
|
||||||
return cast(Qt.DropActions, Qt.MoveAction)
|
|
||||||
|
|
||||||
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
|
||||||
if not index.isValid():
|
|
||||||
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
|
|
||||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
|
||||||
item: SidebarItem = index.internalPointer()
|
|
||||||
if item.item_type in self.sidebar.valid_drop_types:
|
|
||||||
flags |= Qt.ItemIsDropEnabled
|
|
||||||
if item.item_type.is_editable():
|
|
||||||
flags |= Qt.ItemIsEditable
|
|
||||||
|
|
||||||
return cast(Qt.ItemFlags, flags)
|
|
||||||
|
|
||||||
|
|
||||||
class SidebarToolbar(QToolBar):
|
|
||||||
_tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = (
|
|
||||||
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
|
|
||||||
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, sidebar: SidebarTreeView) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.sidebar = sidebar
|
|
||||||
self._action_group = QActionGroup(self)
|
|
||||||
qconnect(self._action_group.triggered, self._on_action_group_triggered)
|
|
||||||
self._setup_tools()
|
|
||||||
self.setIconSize(QSize(16, 16))
|
|
||||||
self.setStyle(QStyleFactory.create("fusion"))
|
|
||||||
|
|
||||||
def _setup_tools(self) -> None:
|
|
||||||
for row, tool in enumerate(self._tools):
|
|
||||||
action = self.addAction(
|
|
||||||
theme_manager.icon_from_resources(tool[1]), tool[2]()
|
|
||||||
)
|
|
||||||
action.setCheckable(True)
|
|
||||||
action.setShortcut(f"Alt+{row + 1}")
|
|
||||||
self._action_group.addAction(action)
|
|
||||||
saved = self.sidebar.col.get_config("sidebarTool", 0)
|
|
||||||
active = saved if saved < len(self._tools) else 0
|
|
||||||
self._action_group.actions()[active].setChecked(True)
|
|
||||||
self.sidebar.tool = self._tools[active][0]
|
|
||||||
|
|
||||||
def _on_action_group_triggered(self, action: QAction) -> None:
|
|
||||||
index = self._action_group.actions().index(action)
|
|
||||||
self.sidebar.col.set_config("sidebarTool", index)
|
|
||||||
self.sidebar.tool = self._tools[index][0]
|
|
||||||
|
|
||||||
|
|
||||||
class SidebarSearchBar(QLineEdit):
|
|
||||||
def __init__(self, sidebar: SidebarTreeView) -> None:
|
|
||||||
QLineEdit.__init__(self, sidebar)
|
|
||||||
self.setPlaceholderText(sidebar.col.tr.browsing_sidebar_filter())
|
|
||||||
self.sidebar = sidebar
|
|
||||||
self.timer = QTimer(self)
|
|
||||||
self.timer.setInterval(600)
|
|
||||||
self.timer.setSingleShot(True)
|
|
||||||
self.setFrame(False)
|
|
||||||
border = theme_manager.color(colors.MEDIUM_BORDER)
|
|
||||||
styles = [
|
|
||||||
"padding: 1px",
|
|
||||||
"padding-left: 3px",
|
|
||||||
f"border-bottom: 1px solid {border}",
|
|
||||||
]
|
|
||||||
if _want_right_border():
|
|
||||||
styles.append(
|
|
||||||
f"border-right: 1px solid {border}",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setStyleSheet("QLineEdit { %s }" % ";".join(styles))
|
|
||||||
|
|
||||||
qconnect(self.timer.timeout, self.onSearch)
|
|
||||||
qconnect(self.textChanged, self.onTextChanged)
|
|
||||||
|
|
||||||
def onTextChanged(self, text: str) -> None:
|
|
||||||
if not self.timer.isActive():
|
|
||||||
self.timer.start()
|
|
||||||
|
|
||||||
def onSearch(self) -> None:
|
|
||||||
self.sidebar.search_for(self.text())
|
|
||||||
|
|
||||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
|
||||||
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
|
||||||
self.sidebar.setFocus()
|
|
||||||
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
|
||||||
self.onSearch()
|
|
||||||
else:
|
|
||||||
QLineEdit.keyPressEvent(self, evt)
|
|
||||||
|
|
||||||
|
|
||||||
def _want_right_border() -> bool:
|
|
||||||
return not isMac or theme_manager.night_mode
|
|
||||||
|
|
||||||
|
|
||||||
# fixme: we should have a top-level Sidebar class inheriting from QWidget that
|
# fixme: we should have a top-level Sidebar class inheriting from QWidget that
|
||||||
# handles the treeview, search bar and so on. Currently the treeview embeds the
|
# handles the treeview, search bar and so on. Currently the treeview embeds the
|
||||||
# search bar which is wrong, and the layout code is handled in browser.py instead
|
# search bar which is wrong, and the layout code is handled in browser.py instead
|
|
@ -343,8 +343,8 @@ hooks = [
|
||||||
name="browser_will_build_tree",
|
name="browser_will_build_tree",
|
||||||
args=[
|
args=[
|
||||||
"handled: bool",
|
"handled: bool",
|
||||||
"tree: aqt.sidebar.SidebarItem",
|
"tree: aqt.browser.SidebarItem",
|
||||||
"stage: aqt.sidebar.SidebarStage",
|
"stage: aqt.browser.SidebarStage",
|
||||||
"browser: aqt.browser.Browser",
|
"browser: aqt.browser.Browser",
|
||||||
],
|
],
|
||||||
return_type="bool",
|
return_type="bool",
|
||||||
|
@ -355,7 +355,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.sidebar.SidebarStage.
|
aqt.browser.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,
|
||||||
|
|
Loading…
Reference in a new issue