2020-10-10 03:42:49 +02:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2020-10-10 03:42:49 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-02-28 21:03:19 +01:00
|
|
|
import re
|
2021-01-04 05:13:20 +01:00
|
|
|
from concurrent.futures import Future
|
2021-02-05 06:26:12 +01:00
|
|
|
from enum import Enum, auto
|
2021-02-03 04:31:46 +01:00
|
|
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
import aqt
|
2021-02-25 21:24:11 +01:00
|
|
|
from anki.collection import Config, SearchJoiner, SearchNode
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki.decks import DeckTreeNode
|
|
|
|
from anki.errors import DeckRenameError, InvalidInput
|
|
|
|
from anki.tags import TagTreeNode
|
2021-02-05 06:26:12 +01:00
|
|
|
from anki.types import assert_exhaustive
|
|
|
|
from aqt import colors, gui_hooks
|
2021-01-03 08:36:54 +01:00
|
|
|
from aqt.main import ResetReason
|
2021-01-20 00:00:53 +01:00
|
|
|
from aqt.models import Models
|
2020-10-10 03:42:49 +02:00
|
|
|
from aqt.qt import *
|
2021-02-05 06:26:12 +01:00
|
|
|
from aqt.theme import ColoredIcon, theme_manager
|
2021-01-29 14:05:30 +01:00
|
|
|
from aqt.utils import (
|
|
|
|
TR,
|
|
|
|
askUser,
|
|
|
|
getOnlyText,
|
|
|
|
show_invalid_search_error,
|
|
|
|
showInfo,
|
|
|
|
showWarning,
|
|
|
|
tr,
|
|
|
|
)
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
|
2021-02-25 11:03:57 +01:00
|
|
|
class SidebarTool(Enum):
|
|
|
|
SELECT = auto()
|
|
|
|
SEARCH = auto()
|
2021-02-25 13:12:51 +01:00
|
|
|
EDIT = auto()
|
2021-02-25 11:03:57 +01:00
|
|
|
|
|
|
|
|
2020-10-10 03:42:49 +02:00
|
|
|
class SidebarItemType(Enum):
|
2021-02-05 06:26:12 +01:00
|
|
|
ROOT = auto()
|
|
|
|
SAVED_SEARCH_ROOT = auto()
|
|
|
|
SAVED_SEARCH = auto()
|
2021-02-09 00:50:59 +01:00
|
|
|
TODAY_ROOT = auto()
|
|
|
|
TODAY = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
FLAG_ROOT = auto()
|
|
|
|
FLAG = auto()
|
2021-02-09 00:54:46 +01:00
|
|
|
CARD_STATE_ROOT = auto()
|
|
|
|
CARD_STATE = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
DECK_ROOT = auto()
|
2021-03-03 09:15:36 +01:00
|
|
|
DECK_CURRENT = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
DECK = auto()
|
|
|
|
NOTETYPE_ROOT = auto()
|
|
|
|
NOTETYPE = auto()
|
|
|
|
NOTETYPE_TEMPLATE = auto()
|
|
|
|
TAG_ROOT = auto()
|
2021-02-09 03:50:35 +01:00
|
|
|
TAG_NONE = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
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()
|
|
|
|
|
2021-03-03 09:15:36 +01:00
|
|
|
def is_editable(self) -> bool:
|
2021-03-03 09:20:02 +01:00
|
|
|
return self in (
|
|
|
|
SidebarItemType.SAVED_SEARCH,
|
|
|
|
SidebarItemType.DECK,
|
|
|
|
SidebarItemType.TAG,
|
2021-03-03 15:18:50 +01:00
|
|
|
SidebarItemType.NOTETYPE,
|
2021-03-03 18:09:53 +01:00
|
|
|
SidebarItemType.NOTETYPE_TEMPLATE,
|
2021-03-03 09:20:02 +01:00
|
|
|
)
|
|
|
|
|
2021-03-04 18:31:35 +01:00
|
|
|
def is_deletable(self) -> bool:
|
|
|
|
return self in (
|
|
|
|
SidebarItemType.SAVED_SEARCH,
|
|
|
|
SidebarItemType.DECK,
|
|
|
|
SidebarItemType.TAG,
|
|
|
|
)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
class SidebarStage(Enum):
|
2021-02-05 06:26:12 +01:00
|
|
|
ROOT = auto()
|
|
|
|
SAVED_SEARCHES = auto()
|
2021-02-09 00:50:59 +01:00
|
|
|
TODAY = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
FLAGS = auto()
|
2021-02-09 00:54:46 +01:00
|
|
|
CARD_STATE = auto()
|
2021-02-05 06:26:12 +01:00
|
|
|
DECKS = auto()
|
|
|
|
NOTETYPES = auto()
|
|
|
|
TAGS = auto()
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
class SidebarItem:
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name: str,
|
2021-02-05 06:26:12 +01:00
|
|
|
icon: Union[str, ColoredIcon],
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node: Optional[SearchNode] = None,
|
2021-02-05 06:26:12 +01:00
|
|
|
on_expanded: Callable[[bool], None] = None,
|
2021-01-23 10:59:12 +01:00
|
|
|
expanded: bool = False,
|
|
|
|
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
|
|
|
id: int = 0,
|
|
|
|
full_name: str = None,
|
|
|
|
) -> None:
|
|
|
|
self.name = name
|
|
|
|
if not full_name:
|
|
|
|
full_name = name
|
|
|
|
self.full_name = full_name
|
|
|
|
self.icon = icon
|
|
|
|
self.item_type = item_type
|
|
|
|
self.id = id
|
2021-02-25 19:57:12 +01:00
|
|
|
self.search_node = search_node
|
2021-02-05 06:26:12 +01:00
|
|
|
self.on_expanded = on_expanded
|
2021-01-23 10:59:12 +01:00
|
|
|
self.children: List["SidebarItem"] = []
|
|
|
|
self.tooltip: Optional[str] = None
|
2021-02-05 06:26:12 +01:00
|
|
|
self._parent_item: Optional["SidebarItem"] = None
|
2021-03-03 11:43:31 +01:00
|
|
|
self._expanded = expanded
|
2021-02-05 06:26:12 +01:00
|
|
|
self._row_in_parent: Optional[int] = None
|
2021-02-02 01:40:50 +01:00
|
|
|
self._search_matches_self = False
|
|
|
|
self._search_matches_child = False
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def add_child(self, cb: "SidebarItem") -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
self.children.append(cb)
|
2021-02-05 06:26:12 +01:00
|
|
|
cb._parent_item = self
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def add_simple(
|
|
|
|
self,
|
|
|
|
name: Union[str, TR.V],
|
|
|
|
icon: Union[str, ColoredIcon],
|
|
|
|
type: SidebarItemType,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node: Optional[SearchNode],
|
2021-02-05 06:26:12 +01:00
|
|
|
) -> SidebarItem:
|
|
|
|
"Add child sidebar item, and return it."
|
|
|
|
if not isinstance(name, str):
|
|
|
|
name = tr(name)
|
|
|
|
item = SidebarItem(
|
|
|
|
name=name,
|
|
|
|
icon=icon,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=search_node,
|
2021-02-05 06:26:12 +01:00
|
|
|
item_type=type,
|
|
|
|
)
|
|
|
|
self.add_child(item)
|
|
|
|
return item
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-03-03 11:43:31 +01:00
|
|
|
@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:
|
2021-02-02 01:40:50 +01:00
|
|
|
if not searching:
|
2021-03-03 11:43:31 +01:00
|
|
|
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()
|
2021-02-02 01:40:50 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
class SidebarModel(QAbstractItemModel):
|
2021-02-28 21:03:19 +01:00
|
|
|
def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
super().__init__()
|
2021-02-28 21:03:19 +01:00
|
|
|
self.sidebar = sidebar
|
2021-01-23 10:59:12 +01:00
|
|
|
self.root = root
|
2021-01-28 09:51:18 +01:00
|
|
|
self._cache_rows(root)
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def _cache_rows(self, node: SidebarItem) -> None:
|
2021-01-28 09:51:18 +01:00
|
|
|
"Cache index of children in parent."
|
|
|
|
for row, item in enumerate(node.children):
|
2021-02-05 06:26:12 +01:00
|
|
|
item._row_in_parent = row
|
2021-01-28 09:51:18 +01:00
|
|
|
self._cache_rows(item)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-01-28 10:10:06 +01:00
|
|
|
def item_for_index(self, idx: QModelIndex) -> SidebarItem:
|
|
|
|
return idx.internalPointer()
|
|
|
|
|
2021-03-02 23:13:34 +01:00
|
|
|
def index_for_item(self, item: SidebarItem) -> QModelIndex:
|
|
|
|
return self.createIndex(item._row_in_parent, 0, item)
|
|
|
|
|
2021-02-02 03:41:45 +01:00
|
|
|
def search(self, text: str) -> bool:
|
|
|
|
return self.root.search(text.lower())
|
2021-02-02 01:40:50 +01:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
# 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()
|
2021-02-05 06:26:12 +01:00
|
|
|
parentItem = childItem._parent_item
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
if parentItem is None or parentItem == self.root:
|
|
|
|
return QModelIndex()
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
row = parentItem._row_in_parent
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
return self.createIndex(row, 0, parentItem)
|
|
|
|
|
|
|
|
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
|
|
|
|
if not index.isValid():
|
|
|
|
return QVariant()
|
|
|
|
|
2021-02-28 21:03:19 +01:00
|
|
|
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole):
|
2021-01-23 10:59:12 +01:00
|
|
|
return QVariant()
|
|
|
|
|
|
|
|
item: SidebarItem = index.internalPointer()
|
|
|
|
|
2021-02-28 21:03:19 +01:00
|
|
|
if role in (Qt.DisplayRole, Qt.EditRole):
|
2021-01-23 10:59:12 +01:00
|
|
|
return QVariant(item.name)
|
2021-02-28 21:03:19 +01:00
|
|
|
if role == Qt.ToolTipRole:
|
2021-01-23 10:59:12 +01:00
|
|
|
return QVariant(item.tooltip)
|
2021-02-28 21:03:19 +01:00
|
|
|
return QVariant(theme_manager.icon_from_resources(item.icon))
|
|
|
|
|
2021-02-28 22:36:31 +01:00
|
|
|
def setData(
|
|
|
|
self, index: QModelIndex, text: QVariant, _role: int = Qt.EditRole
|
|
|
|
) -> bool:
|
2021-02-28 21:03:19 +01:00
|
|
|
return self.sidebar.rename_node(index.internalPointer(), text)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def supportedDropActions(self) -> Qt.DropActions:
|
|
|
|
return cast(Qt.DropActions, Qt.MoveAction)
|
2021-01-30 12:08:39 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
2021-01-30 12:08:39 +01:00
|
|
|
if not index.isValid():
|
2021-02-01 00:51:46 +01:00
|
|
|
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
|
2021-01-30 12:32:25 +01:00
|
|
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
|
|
|
|
|
|
|
item: SidebarItem = index.internalPointer()
|
|
|
|
if item.item_type in (
|
|
|
|
SidebarItemType.DECK,
|
|
|
|
SidebarItemType.DECK_ROOT,
|
2021-02-02 11:14:04 +01:00
|
|
|
SidebarItemType.TAG,
|
|
|
|
SidebarItemType.TAG_ROOT,
|
2021-01-30 12:32:25 +01:00
|
|
|
):
|
|
|
|
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
2021-03-03 09:15:36 +01:00
|
|
|
if item.item_type.is_editable():
|
2021-02-28 21:03:19 +01:00
|
|
|
flags |= Qt.ItemIsEditable
|
2021-01-30 12:32:25 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
return cast(Qt.ItemFlags, flags)
|
2021-01-30 12:08:39 +01:00
|
|
|
|
2021-01-28 10:10:06 +01:00
|
|
|
|
2021-02-25 11:03:57 +01:00
|
|
|
class SidebarToolbar(QToolBar):
|
2021-02-25 19:28:29 +01:00
|
|
|
_tools: Tuple[Tuple[SidebarTool, str, str], ...] = (
|
2021-02-25 11:03:57 +01:00
|
|
|
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", "search"),
|
2021-02-25 19:28:29 +01:00
|
|
|
(SidebarTool.SELECT, ":/icons/select.svg", "select"),
|
2021-02-25 13:12:51 +01:00
|
|
|
(SidebarTool.EDIT, ":/icons/edit.svg", "edit"),
|
2021-02-25 11:03:57 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
2021-02-25 11:35:31 +01:00
|
|
|
self._setup_tools()
|
2021-02-26 13:04:30 +01:00
|
|
|
self.setIconSize(QSize(18, 18))
|
2021-02-25 11:03:57 +01:00
|
|
|
|
2021-02-25 11:35:31 +01:00
|
|
|
def _setup_tools(self) -> None:
|
2021-02-25 11:03:57 +01:00
|
|
|
for row in self._tools:
|
|
|
|
action = self.addAction(theme_manager.icon_from_resources(row[1]), row[2])
|
|
|
|
action.setCheckable(True)
|
|
|
|
self._action_group.addAction(action)
|
2021-02-25 11:35:31 +01:00
|
|
|
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]
|
2021-02-25 11:03:57 +01:00
|
|
|
|
2021-02-25 19:28:29 +01:00
|
|
|
def _on_action_group_triggered(self, action: QAction) -> None:
|
2021-02-25 11:35:31 +01:00
|
|
|
index = self._action_group.actions().index(action)
|
|
|
|
self.sidebar.col.set_config("sidebarTool", index)
|
|
|
|
self.sidebar.tool = self._tools[index][0]
|
2021-02-25 11:06:59 +01:00
|
|
|
|
|
|
|
|
2021-01-26 23:35:40 +01:00
|
|
|
class SidebarSearchBar(QLineEdit):
|
2021-02-01 00:51:46 +01:00
|
|
|
def __init__(self, sidebar: SidebarTreeView) -> None:
|
2021-01-26 23:35:40 +01:00
|
|
|
QLineEdit.__init__(self, sidebar)
|
2021-01-29 13:10:31 +01:00
|
|
|
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
|
2021-01-26 23:35:40 +01:00
|
|
|
self.sidebar = sidebar
|
2021-01-29 01:40:32 +01:00
|
|
|
self.timer = QTimer(self)
|
|
|
|
self.timer.setInterval(600)
|
|
|
|
self.timer.setSingleShot(True)
|
2021-02-09 07:02:18 +01:00
|
|
|
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))
|
|
|
|
|
2021-01-29 01:40:32 +01:00
|
|
|
qconnect(self.timer.timeout, self.onSearch)
|
2021-01-26 23:35:40 +01:00
|
|
|
qconnect(self.textChanged, self.onTextChanged)
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def onTextChanged(self, text: str) -> None:
|
2021-01-29 01:40:32 +01:00
|
|
|
if not self.timer.isActive():
|
|
|
|
self.timer.start()
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def onSearch(self) -> None:
|
2021-01-29 01:40:32 +01:00
|
|
|
self.sidebar.search_for(self.text())
|
2021-01-26 23:35:40 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
2021-01-26 23:35:40 +01:00
|
|
|
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
|
|
|
self.sidebar.setFocus()
|
|
|
|
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
2021-01-29 01:40:32 +01:00
|
|
|
self.onSearch()
|
2021-01-26 23:35:40 +01:00
|
|
|
else:
|
|
|
|
QLineEdit.keyPressEvent(self, evt)
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-09 07:02:18 +01:00
|
|
|
def _want_right_border() -> bool:
|
|
|
|
return not isMac or theme_manager.night_mode
|
|
|
|
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
class SidebarTreeView(QTreeView):
|
|
|
|
def __init__(self, browser: aqt.browser.Browser) -> None:
|
2020-10-10 03:42:49 +02:00
|
|
|
super().__init__()
|
2021-01-23 10:59:12 +01:00
|
|
|
self.browser = browser
|
|
|
|
self.mw = browser.mw
|
|
|
|
self.col = self.mw.col
|
2021-01-28 10:10:06 +01:00
|
|
|
self.current_search: Optional[str] = None
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-11 01:12:03 +01:00
|
|
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
|
self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore
|
2021-01-29 13:50:29 +01:00
|
|
|
self.context_menus: Dict[SidebarItemType, Sequence[Tuple[str, Callable]]] = {
|
2021-01-23 10:59:12 +01:00
|
|
|
SidebarItemType.NOTETYPE: ((tr(TR.ACTIONS_MANAGE), self.manage_notetype),),
|
2021-01-29 13:10:31 +01:00
|
|
|
SidebarItemType.SAVED_SEARCH_ROOT: (
|
|
|
|
(tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search),
|
|
|
|
),
|
2021-01-23 10:59:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
self.setUniformRowHeights(True)
|
|
|
|
self.setHeaderHidden(True)
|
|
|
|
self.setIndentation(15)
|
2021-02-02 11:14:14 +01:00
|
|
|
self.setAutoExpandDelay(600)
|
2021-01-30 12:08:39 +01:00
|
|
|
self.setDragDropOverwriteMode(False)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
qconnect(self.expanded, self._on_expansion)
|
|
|
|
qconnect(self.collapsed, self._on_collapse)
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-02-09 07:02:18 +01:00
|
|
|
# match window background color and tweak style
|
2021-01-23 10:59:12 +01:00
|
|
|
bgcolor = QPalette().window().color().name()
|
2021-02-09 07:02:18 +01:00
|
|
|
border = theme_manager.color(colors.MEDIUM_BORDER)
|
|
|
|
styles = [
|
|
|
|
"padding: 3px",
|
|
|
|
"padding-right: 0px",
|
|
|
|
"border: 0",
|
|
|
|
f"background: {bgcolor}",
|
|
|
|
]
|
|
|
|
if _want_right_border():
|
|
|
|
styles.append(f"border-right: 1px solid {border}")
|
|
|
|
|
|
|
|
self.setStyleSheet("QTreeView { %s }" % ";".join(styles))
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-25 11:06:59 +01:00
|
|
|
@property
|
|
|
|
def tool(self) -> SidebarTool:
|
|
|
|
return self._tool
|
|
|
|
|
|
|
|
@tool.setter
|
|
|
|
def tool(self, tool: SidebarTool) -> None:
|
|
|
|
self._tool = tool
|
|
|
|
if tool == SidebarTool.SELECT:
|
2021-02-28 22:36:31 +01:00
|
|
|
selection_mode = QAbstractItemView.ExtendedSelection
|
2021-02-25 13:12:51 +01:00
|
|
|
drag_drop_mode = QAbstractItemView.NoDragDrop
|
2021-02-28 21:03:19 +01:00
|
|
|
edit_triggers = QAbstractItemView.EditKeyPressed
|
2021-02-25 11:06:59 +01:00
|
|
|
elif tool == SidebarTool.SEARCH:
|
2021-02-28 22:36:31 +01:00
|
|
|
selection_mode = QAbstractItemView.SingleSelection
|
2021-02-25 13:12:51 +01:00
|
|
|
drag_drop_mode = QAbstractItemView.NoDragDrop
|
2021-02-28 21:03:19 +01:00
|
|
|
edit_triggers = QAbstractItemView.EditKeyPressed
|
2021-02-25 13:12:51 +01:00
|
|
|
elif tool == SidebarTool.EDIT:
|
2021-03-01 08:45:03 +01:00
|
|
|
selection_mode = QAbstractItemView.ExtendedSelection
|
2021-02-25 13:12:51 +01:00
|
|
|
drag_drop_mode = QAbstractItemView.InternalMove
|
2021-02-28 22:36:31 +01:00
|
|
|
edit_triggers = cast(
|
|
|
|
QAbstractItemView.EditTriggers,
|
|
|
|
QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed,
|
|
|
|
)
|
2021-02-25 13:12:51 +01:00
|
|
|
self.setSelectionMode(selection_mode)
|
|
|
|
self.setDragDropMode(drag_drop_mode)
|
2021-02-28 21:03:19 +01:00
|
|
|
self.setEditTriggers(edit_triggers)
|
2021-02-25 11:06:59 +01:00
|
|
|
|
2021-02-02 01:40:50 +01:00
|
|
|
def model(self) -> SidebarModel:
|
2021-01-28 10:10:06 +01:00
|
|
|
return super().model()
|
|
|
|
|
2021-03-02 23:13:34 +01:00
|
|
|
def refresh(
|
|
|
|
self, is_current: Optional[Callable[[SidebarItem], bool]] = None
|
|
|
|
) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
"Refresh list. No-op if sidebar is not visible."
|
|
|
|
if not self.isVisible():
|
|
|
|
return
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def on_done(fut: Future) -> None:
|
2021-01-26 00:02:08 +01:00
|
|
|
root = fut.result()
|
2021-02-28 21:03:19 +01:00
|
|
|
model = SidebarModel(self, root)
|
2021-01-28 08:47:58 +01:00
|
|
|
|
2021-01-28 09:51:18 +01:00
|
|
|
# from PyQt5.QtTest import QAbstractItemModelTester
|
|
|
|
# tester = QAbstractItemModelTester(model)
|
2021-01-28 08:47:58 +01:00
|
|
|
|
2021-01-28 09:51:18 +01:00
|
|
|
self.setModel(model)
|
2021-01-29 02:20:15 +01:00
|
|
|
if self.current_search:
|
|
|
|
self.search_for(self.current_search)
|
|
|
|
else:
|
2021-02-05 06:26:12 +01:00
|
|
|
self._expand_where_necessary(model)
|
2021-02-28 21:03:19 +01:00
|
|
|
self.setUpdatesEnabled(True)
|
2021-03-02 23:13:34 +01:00
|
|
|
if is_current:
|
|
|
|
self.restore_current(is_current)
|
2021-01-26 00:02:08 +01:00
|
|
|
|
2021-02-28 21:03:19 +01:00
|
|
|
# block repainting during refreshing to avoid flickering
|
|
|
|
self.setUpdatesEnabled(False)
|
2021-01-26 00:02:08 +01:00
|
|
|
self.mw.taskman.run_in_background(self._root_tree, on_done)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-03-02 23:13:34 +01:00
|
|
|
def restore_current(self, is_current: Callable[[SidebarItem], bool]) -> None:
|
|
|
|
if current := self.find_item(is_current):
|
|
|
|
index = self.model().index_for_item(current)
|
2021-03-03 23:00:37 +01:00
|
|
|
self.selectionModel().setCurrentIndex(
|
|
|
|
index, QItemSelectionModel.SelectCurrent
|
|
|
|
)
|
2021-03-02 23:13:34 +01:00
|
|
|
self.scrollTo(index)
|
|
|
|
|
|
|
|
def find_item(
|
|
|
|
self,
|
|
|
|
is_target: Callable[[SidebarItem], bool],
|
|
|
|
parent: Optional[SidebarItem] = None,
|
|
|
|
) -> Optional[SidebarItem]:
|
|
|
|
def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]:
|
|
|
|
if is_target(parent):
|
|
|
|
return parent
|
|
|
|
for child in parent.children:
|
|
|
|
if item := find_item_rec(child):
|
|
|
|
return item
|
|
|
|
return None
|
|
|
|
|
|
|
|
return find_item_rec(parent or self.model().root)
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def search_for(self, text: str) -> None:
|
2021-02-02 03:41:45 +01:00
|
|
|
self.showColumn(0)
|
2021-01-28 09:51:18 +01:00
|
|
|
if not text.strip():
|
2021-01-29 02:20:15 +01:00
|
|
|
self.current_search = None
|
2021-01-28 09:51:18 +01:00
|
|
|
self.refresh()
|
|
|
|
return
|
|
|
|
|
2021-01-28 09:58:51 +01:00
|
|
|
self.current_search = text
|
2021-02-02 01:40:50 +01:00
|
|
|
# start from a collapsed state, as it's faster
|
2021-01-28 09:51:18 +01:00
|
|
|
self.collapseAll()
|
2021-02-02 03:41:45 +01:00
|
|
|
self.setColumnHidden(0, not self.model().search(text))
|
2021-02-05 06:26:12 +01:00
|
|
|
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):
|
2021-03-03 11:43:31 +01:00
|
|
|
if item.show_expanded(searching):
|
2021-02-05 06:26:12 +01:00
|
|
|
self.setExpanded(idx, True)
|
|
|
|
|
2021-02-25 21:24:11 +01:00
|
|
|
def update_search(
|
|
|
|
self,
|
|
|
|
*terms: Union[str, SearchNode],
|
|
|
|
joiner: SearchJoiner = "AND",
|
|
|
|
) -> None:
|
2021-02-11 08:11:17 +01:00
|
|
|
"""Modify the current search string based on modifier keys, then refresh."""
|
|
|
|
mods = self.mw.app.keyboardModifiers()
|
2021-02-11 10:57:19 +01:00
|
|
|
previous = SearchNode(parsable_text=self.browser.current_search())
|
2021-02-25 21:24:11 +01:00
|
|
|
current = self.mw.col.group_searches(*terms, joiner=joiner)
|
2021-02-11 08:11:17 +01:00
|
|
|
|
|
|
|
# if Alt pressed, invert
|
|
|
|
if mods & Qt.AltModifier:
|
2021-02-11 10:57:19 +01:00
|
|
|
current = SearchNode(negated=current)
|
2021-02-11 08:11:17 +01:00
|
|
|
|
2021-02-09 01:50:39 +01:00
|
|
|
try:
|
|
|
|
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
2021-02-11 08:11:17 +01:00
|
|
|
# If Ctrl+Shift, replace searches nodes of the same type.
|
2021-02-11 10:57:19 +01:00
|
|
|
search = self.col.replace_in_search_node(previous, current)
|
2021-02-09 01:50:39 +01:00
|
|
|
elif mods & Qt.ControlModifier:
|
2021-02-11 08:11:17 +01:00
|
|
|
# If Ctrl, AND with previous
|
|
|
|
search = self.col.join_searches(previous, current, "AND")
|
2021-02-09 01:50:39 +01:00
|
|
|
elif mods & Qt.ShiftModifier:
|
2021-02-11 08:11:17 +01:00
|
|
|
# If Shift, OR with previous
|
|
|
|
search = self.col.join_searches(previous, current, "OR")
|
|
|
|
else:
|
|
|
|
search = self.col.build_search_string(current)
|
2021-02-09 01:50:39 +01:00
|
|
|
except InvalidInput as e:
|
|
|
|
show_invalid_search_error(e)
|
|
|
|
else:
|
|
|
|
self.browser.search_for(search)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
# Qt API
|
|
|
|
###########
|
2021-01-28 09:51:18 +01:00
|
|
|
|
2021-01-28 10:10:06 +01:00
|
|
|
def drawRow(
|
|
|
|
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
|
2021-02-01 00:51:46 +01:00
|
|
|
) -> None:
|
2021-02-02 01:40:50 +01:00
|
|
|
if self.current_search and (item := self.model().item_for_index(idx)):
|
|
|
|
if item.is_highlighted():
|
2021-02-05 09:50:01 +01:00
|
|
|
brush = QBrush(theme_manager.qcolor(colors.SUSPENDED_BG))
|
2021-02-02 01:40:50 +01:00
|
|
|
painter.save()
|
|
|
|
painter.fillRect(options.rect, brush)
|
|
|
|
painter.restore()
|
2021-01-28 09:58:51 +01:00
|
|
|
return super().drawRow(painter, options, idx)
|
|
|
|
|
2021-01-30 12:08:39 +01:00
|
|
|
def dropEvent(self, event: QDropEvent) -> None:
|
|
|
|
model = self.model()
|
|
|
|
target_item = model.item_for_index(self.indexAt(event.pos()))
|
2021-02-25 21:24:11 +01:00
|
|
|
if self.handle_drag_drop(self._selected_items(), target_item):
|
2021-01-30 12:08:39 +01:00
|
|
|
event.acceptProposedAction()
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
|
|
|
super().mouseReleaseEvent(event)
|
2021-02-25 11:06:59 +01:00
|
|
|
if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton:
|
2021-02-20 14:58:02 +01:00
|
|
|
idx = self.indexAt(event.pos())
|
2021-03-04 18:31:35 +01:00
|
|
|
self._on_search(idx)
|
2021-02-05 06:26:12 +01:00
|
|
|
|
|
|
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
2021-03-04 18:31:35 +01:00
|
|
|
index = self.currentIndex()
|
2021-02-05 06:26:12 +01:00
|
|
|
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
2021-03-04 18:31:35 +01:00
|
|
|
if not self.isPersistentEditorOpen(index):
|
|
|
|
self._on_search(index)
|
|
|
|
elif event.key() == Qt.Key_Delete:
|
|
|
|
self._on_delete(index)
|
2021-02-05 06:26:12 +01:00
|
|
|
else:
|
|
|
|
super().keyPressEvent(event)
|
|
|
|
|
|
|
|
###########
|
|
|
|
|
2021-01-30 12:08:39 +01:00
|
|
|
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)
|
2021-02-02 11:14:04 +01:00
|
|
|
if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT):
|
|
|
|
return self._handle_drag_drop_tags(sources, target)
|
2021-01-30 12:08:39 +01:00
|
|
|
return False
|
|
|
|
|
2021-01-30 12:32:25 +01:00
|
|
|
def _handle_drag_drop_decks(
|
|
|
|
self, sources: List[SidebarItem], target: SidebarItem
|
|
|
|
) -> bool:
|
|
|
|
source_ids = [
|
|
|
|
source.id for source in sources if source.item_type == SidebarItemType.DECK
|
|
|
|
]
|
2021-01-30 12:08:39 +01:00
|
|
|
if not source_ids:
|
|
|
|
return False
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def on_done(fut: Future) -> None:
|
2021-02-02 12:45:54 +01:00
|
|
|
self.browser.model.endReset()
|
2021-01-30 12:08:39 +01:00
|
|
|
fut.result()
|
|
|
|
self.refresh()
|
2021-02-02 12:45:54 +01:00
|
|
|
self.mw.deckBrowser.refresh()
|
2021-01-30 12:08:39 +01:00
|
|
|
|
2021-02-02 12:45:54 +01:00
|
|
|
def on_save() -> None:
|
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK))
|
|
|
|
self.browser.model.beginReset()
|
|
|
|
self.mw.taskman.with_progress(
|
|
|
|
lambda: self.col.decks.drag_drop_decks(source_ids, target.id), on_done
|
|
|
|
)
|
|
|
|
|
|
|
|
self.browser.editor.saveNow(on_save)
|
2021-01-30 12:08:39 +01:00
|
|
|
return True
|
|
|
|
|
2021-02-02 11:14:04 +01:00
|
|
|
def _handle_drag_drop_tags(
|
|
|
|
self, sources: List[SidebarItem], target: SidebarItem
|
|
|
|
) -> bool:
|
|
|
|
source_ids = [
|
|
|
|
source.full_name
|
|
|
|
for source in sources
|
|
|
|
if source.item_type == SidebarItemType.TAG
|
|
|
|
]
|
|
|
|
if not source_ids:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def on_done(fut: Future) -> None:
|
2021-02-02 12:45:54 +01:00
|
|
|
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
|
|
|
self.browser.model.endReset()
|
2021-02-02 11:14:04 +01:00
|
|
|
fut.result()
|
|
|
|
self.refresh()
|
|
|
|
|
|
|
|
if target.item_type == SidebarItemType.TAG_ROOT:
|
|
|
|
target_name = ""
|
|
|
|
else:
|
|
|
|
target_name = target.full_name
|
|
|
|
|
2021-02-02 12:45:54 +01:00
|
|
|
def on_save() -> None:
|
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME_TAG))
|
|
|
|
self.browser.model.beginReset()
|
|
|
|
self.mw.taskman.with_progress(
|
|
|
|
lambda: self.col.tags.drag_drop(source_ids, target_name), on_done
|
|
|
|
)
|
|
|
|
|
|
|
|
self.browser.editor.saveNow(on_save)
|
2021-02-02 11:14:04 +01:00
|
|
|
return True
|
|
|
|
|
2021-03-04 18:31:35 +01:00
|
|
|
def _on_search(self, index: QModelIndex) -> None:
|
2021-03-04 17:40:12 +01:00
|
|
|
if item := self.model().item_for_index(index):
|
|
|
|
if search_node := item.search_node:
|
|
|
|
self.update_search(search_node)
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-03-04 18:31:35 +01:00
|
|
|
def _on_delete(self, index: QModelIndex) -> None:
|
|
|
|
if item := self.model().item_for_index(index):
|
|
|
|
if item.item_type == SidebarItemType.SAVED_SEARCH:
|
|
|
|
self.remove_saved_searches(item)
|
|
|
|
elif item.item_type == SidebarItemType.DECK:
|
|
|
|
self.delete_decks(item)
|
|
|
|
elif item.item_type == SidebarItemType.TAG:
|
|
|
|
self.remove_tags(item)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def _on_expansion(self, idx: QModelIndex) -> None:
|
2021-01-28 09:58:51 +01:00
|
|
|
if self.current_search:
|
2021-01-28 09:51:18 +01:00
|
|
|
return
|
2021-03-03 11:43:31 +01:00
|
|
|
if item := self.model().item_for_index(idx):
|
|
|
|
item.expanded = True
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def _on_collapse(self, idx: QModelIndex) -> None:
|
2021-01-28 09:58:51 +01:00
|
|
|
if self.current_search:
|
2021-01-28 09:51:18 +01:00
|
|
|
return
|
2021-03-03 11:43:31 +01:00
|
|
|
if item := self.model().item_for_index(idx):
|
|
|
|
item.expanded = False
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
# Tree building
|
|
|
|
###########################
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
def _root_tree(self) -> SidebarItem:
|
2021-02-05 06:26:12 +01:00
|
|
|
root: Optional[SidebarItem] = None
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
for stage in SidebarStage:
|
|
|
|
if stage == SidebarStage.ROOT:
|
|
|
|
root = SidebarItem("", "", item_type=SidebarItemType.ROOT)
|
2021-01-23 10:59:12 +01:00
|
|
|
handled = gui_hooks.browser_will_build_tree(False, root, stage, self)
|
2021-02-05 06:26:12 +01:00
|
|
|
if not handled:
|
|
|
|
self._build_stage(root, stage)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
return root
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def _build_stage(self, root: SidebarItem, stage: SidebarStage) -> None:
|
|
|
|
if stage is SidebarStage.SAVED_SEARCHES:
|
|
|
|
self._saved_searches_tree(root)
|
2021-02-09 00:54:46 +01:00
|
|
|
elif stage is SidebarStage.CARD_STATE:
|
2021-02-05 06:26:12 +01:00
|
|
|
self._card_state_tree(root)
|
2021-02-09 00:50:59 +01:00
|
|
|
elif stage is SidebarStage.TODAY:
|
|
|
|
self._today_tree(root)
|
2021-02-05 06:26:12 +01:00
|
|
|
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)
|
|
|
|
|
2021-01-29 13:10:31 +01:00
|
|
|
def _section_root(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
root: SidebarItem,
|
2021-02-03 04:31:46 +01:00
|
|
|
name: TR.V,
|
2021-02-05 06:26:12 +01:00
|
|
|
icon: Union[str, ColoredIcon],
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key: Config.Bool.Key.V,
|
2021-01-29 13:10:31 +01:00
|
|
|
type: Optional[SidebarItemType] = None,
|
|
|
|
) -> SidebarItem:
|
2021-02-01 00:51:46 +01:00
|
|
|
def update(expanded: bool) -> None:
|
2021-01-29 13:10:31 +01:00
|
|
|
self.col.set_config_bool(collapse_key, not expanded)
|
|
|
|
|
|
|
|
top = SidebarItem(
|
2021-01-29 13:50:29 +01:00
|
|
|
tr(name),
|
2021-01-29 13:10:31 +01:00
|
|
|
icon,
|
2021-02-05 06:26:12 +01:00
|
|
|
on_expanded=update,
|
2021-01-29 13:10:31 +01:00
|
|
|
expanded=not self.col.get_config_bool(collapse_key),
|
|
|
|
item_type=type,
|
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_child(top)
|
2021-01-29 13:10:31 +01:00
|
|
|
|
|
|
|
return top
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
# Tree: Saved Searches
|
|
|
|
###########################
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-01-29 13:10:31 +01:00
|
|
|
def _saved_searches_tree(self, root: SidebarItem) -> None:
|
|
|
|
icon = ":/icons/heart.svg"
|
2021-02-05 13:38:44 +01:00
|
|
|
saved = self._get_saved_searches()
|
2021-01-29 13:10:31 +01:00
|
|
|
|
|
|
|
root = self._section_root(
|
|
|
|
root=root,
|
|
|
|
name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES,
|
|
|
|
icon=icon,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_SAVED_SEARCHES,
|
2021-01-29 13:10:31 +01:00
|
|
|
type=SidebarItemType.SAVED_SEARCH_ROOT,
|
|
|
|
)
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
for name, filt in sorted(saved.items()):
|
|
|
|
item = SidebarItem(
|
|
|
|
name,
|
2021-01-29 13:10:31 +01:00
|
|
|
icon,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(parsable_text=filt),
|
2021-02-05 06:26:12 +01:00
|
|
|
item_type=SidebarItemType.SAVED_SEARCH,
|
2021-01-23 10:59:12 +01:00
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_child(item)
|
|
|
|
|
2021-02-09 00:50:59 +01:00
|
|
|
# Tree: Today
|
2021-02-05 06:26:12 +01:00
|
|
|
###########################
|
|
|
|
|
2021-02-09 00:50:59 +01:00
|
|
|
def _today_tree(self, root: SidebarItem) -> None:
|
2021-02-05 06:26:12 +01:00
|
|
|
icon = ":/icons/clock.svg"
|
|
|
|
root = self._section_root(
|
|
|
|
root=root,
|
2021-02-09 00:50:59 +01:00
|
|
|
name=TR.BROWSING_TODAY,
|
2021-02-05 06:26:12 +01:00
|
|
|
icon=icon,
|
2021-02-09 00:50:59 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_TODAY,
|
|
|
|
type=SidebarItemType.TODAY_ROOT,
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
2021-02-09 00:50:59 +01:00
|
|
|
type = SidebarItemType.TODAY
|
2021-02-05 06:26:12 +01:00
|
|
|
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_SIDEBAR_DUE_TODAY,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(due_on_day=0),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_ADDED_TODAY,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(added_in_days=1),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_EDITED_TODAY,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(edited_in_days=1),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_STUDIED_TODAY,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(rated=SearchNode.Rated(days=1)),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_AGAIN_TODAY,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 21:24:11 +01:00
|
|
|
search_node=SearchNode(
|
|
|
|
rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN)
|
|
|
|
),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
2021-02-11 01:49:36 +01:00
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_SIDEBAR_OVERDUE,
|
|
|
|
icon=icon,
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=self.col.group_searches(
|
2021-02-11 10:57:19 +01:00
|
|
|
SearchNode(card_state=SearchNode.CARD_STATE_DUE),
|
|
|
|
SearchNode(negated=SearchNode(due_on_day=0)),
|
2021-02-11 01:49:36 +01:00
|
|
|
),
|
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
|
|
|
|
# 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,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_CARD_STATE,
|
2021-02-05 06:26:12 +01:00
|
|
|
type=SidebarItemType.CARD_STATE_ROOT,
|
|
|
|
)
|
|
|
|
type = SidebarItemType.CARD_STATE
|
|
|
|
|
|
|
|
root.add_simple(
|
|
|
|
TR.ACTIONS_NEW,
|
|
|
|
icon=icon.with_color(colors.NEW_COUNT),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(card_state=SearchNode.CARD_STATE_NEW),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.SCHEDULING_LEARNING,
|
|
|
|
icon=icon.with_color(colors.LEARN_COUNT),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(card_state=SearchNode.CARD_STATE_LEARN),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.SCHEDULING_REVIEW,
|
|
|
|
icon=icon.with_color(colors.REVIEW_COUNT),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(card_state=SearchNode.CARD_STATE_REVIEW),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_SUSPENDED,
|
|
|
|
icon=icon.with_color(colors.SUSPENDED_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
name=TR.BROWSING_BURIED,
|
|
|
|
icon=icon.with_color(colors.BURIED_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(card_state=SearchNode.CARD_STATE_BURIED),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
# 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,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_FLAGS,
|
2021-02-05 06:26:12 +01:00
|
|
|
type=SidebarItemType.FLAG_ROOT,
|
|
|
|
)
|
2021-02-25 19:57:12 +01:00
|
|
|
root.search_node = SearchNode(flag=SearchNode.FLAG_ANY)
|
2021-02-05 06:26:12 +01:00
|
|
|
|
2021-02-08 13:52:37 +01:00
|
|
|
type = SidebarItemType.FLAG
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_simple(
|
|
|
|
TR.ACTIONS_RED_FLAG,
|
|
|
|
icon=icon.with_color(colors.FLAG1_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(flag=SearchNode.FLAG_RED),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
TR.ACTIONS_ORANGE_FLAG,
|
|
|
|
icon=icon.with_color(colors.FLAG2_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(flag=SearchNode.FLAG_ORANGE),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
TR.ACTIONS_GREEN_FLAG,
|
|
|
|
icon=icon.with_color(colors.FLAG3_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(flag=SearchNode.FLAG_GREEN),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
TR.ACTIONS_BLUE_FLAG,
|
|
|
|
icon=icon.with_color(colors.FLAG4_FG),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(flag=SearchNode.FLAG_BLUE),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
root.add_simple(
|
|
|
|
TR.BROWSING_NO_FLAG,
|
|
|
|
icon=icon.with_color(colors.DISABLED),
|
|
|
|
type=type,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(flag=SearchNode.FLAG_NONE),
|
2021-02-05 06:26:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
# Tree: Tags
|
|
|
|
###########################
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
def _tag_tree(self, root: SidebarItem) -> None:
|
2021-01-29 13:10:31 +01:00
|
|
|
icon = ":/icons/tag.svg"
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def render(
|
|
|
|
root: SidebarItem, nodes: Iterable[TagTreeNode], head: str = ""
|
|
|
|
) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
for node in nodes:
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def toggle_expand() -> Callable[[bool], None]:
|
2021-01-23 10:59:12 +01:00
|
|
|
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
2021-02-02 09:49:34 +01:00
|
|
|
return lambda expanded: self.mw.col.tags.set_expanded(
|
|
|
|
full_name, expanded
|
2021-01-23 10:59:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
item = SidebarItem(
|
2021-02-25 19:57:12 +01:00
|
|
|
name=node.name,
|
|
|
|
icon=icon,
|
|
|
|
search_node=SearchNode(tag=head + node.name),
|
|
|
|
on_expanded=toggle_expand(),
|
|
|
|
expanded=node.expanded,
|
2021-01-23 10:59:12 +01:00
|
|
|
item_type=SidebarItemType.TAG,
|
|
|
|
full_name=head + node.name,
|
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_child(item)
|
2021-02-11 01:09:06 +01:00
|
|
|
newhead = f"{head + node.name}::"
|
2021-01-23 10:59:12 +01:00
|
|
|
render(item, node.children, newhead)
|
|
|
|
|
2021-01-31 06:55:08 +01:00
|
|
|
tree = self.col.tags.tree()
|
2021-01-29 13:10:31 +01:00
|
|
|
root = self._section_root(
|
|
|
|
root=root,
|
|
|
|
name=TR.BROWSING_SIDEBAR_TAGS,
|
|
|
|
icon=icon,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_TAGS,
|
2021-02-02 02:51:45 +01:00
|
|
|
type=SidebarItemType.TAG_ROOT,
|
2021-01-29 13:10:31 +01:00
|
|
|
)
|
2021-02-25 19:57:12 +01:00
|
|
|
root.search_node = SearchNode(negated=SearchNode(tag="none"))
|
2021-02-09 03:50:35 +01:00
|
|
|
root.add_simple(
|
|
|
|
name=tr(TR.BROWSING_SIDEBAR_UNTAGGED),
|
|
|
|
icon=icon,
|
|
|
|
type=SidebarItemType.TAG_NONE,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(tag="none"),
|
2021-02-09 03:50:35 +01:00
|
|
|
)
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
render(root, tree.children)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
# Tree: Decks
|
|
|
|
###########################
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
def _deck_tree(self, root: SidebarItem) -> None:
|
2021-01-29 13:10:31 +01:00
|
|
|
icon = ":/icons/deck.svg"
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def render(
|
|
|
|
root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = ""
|
|
|
|
) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
for node in nodes:
|
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def toggle_expand() -> Callable[[bool], None]:
|
2021-01-23 10:59:12 +01:00
|
|
|
did = node.deck_id # pylint: disable=cell-var-from-loop
|
|
|
|
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
|
|
|
|
|
|
|
item = SidebarItem(
|
2021-02-25 19:57:12 +01:00
|
|
|
name=node.name,
|
|
|
|
icon=icon,
|
|
|
|
search_node=SearchNode(deck=head + node.name),
|
|
|
|
on_expanded=toggle_expand(),
|
|
|
|
expanded=not node.collapsed,
|
2021-01-23 10:59:12 +01:00
|
|
|
item_type=SidebarItemType.DECK,
|
|
|
|
id=node.deck_id,
|
2021-01-26 23:35:40 +01:00
|
|
|
full_name=head + node.name,
|
2021-01-23 10:59:12 +01:00
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_child(item)
|
2021-02-11 01:09:06 +01:00
|
|
|
newhead = f"{head + node.name}::"
|
2021-01-23 10:59:12 +01:00
|
|
|
render(item, node.children, newhead)
|
|
|
|
|
2021-01-29 13:10:31 +01:00
|
|
|
tree = self.col.decks.deck_tree()
|
|
|
|
root = self._section_root(
|
|
|
|
root=root,
|
|
|
|
name=TR.BROWSING_SIDEBAR_DECKS,
|
|
|
|
icon=icon,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_DECKS,
|
2021-01-30 12:08:39 +01:00
|
|
|
type=SidebarItemType.DECK_ROOT,
|
2021-01-29 13:10:31 +01:00
|
|
|
)
|
2021-02-25 19:57:12 +01:00
|
|
|
root.search_node = SearchNode(deck="*")
|
2021-02-09 00:38:37 +01:00
|
|
|
current = root.add_simple(
|
2021-02-08 13:48:45 +01:00
|
|
|
name=tr(TR.BROWSING_CURRENT_DECK),
|
|
|
|
icon=icon,
|
2021-03-03 09:15:36 +01:00
|
|
|
type=SidebarItemType.DECK_CURRENT,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(deck="current"),
|
2021-02-08 13:48:45 +01:00
|
|
|
)
|
2021-02-09 00:38:37 +01:00
|
|
|
current.id = self.mw.col.decks.selected()
|
2021-02-08 13:48:45 +01:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
render(root, tree.children)
|
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
# Tree: Notetypes
|
|
|
|
###########################
|
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
def _notetype_tree(self, root: SidebarItem) -> None:
|
2021-01-29 13:10:31 +01:00
|
|
|
icon = ":/icons/notetype.svg"
|
|
|
|
root = self._section_root(
|
|
|
|
root=root,
|
|
|
|
name=TR.BROWSING_SIDEBAR_NOTETYPES,
|
|
|
|
icon=icon,
|
2021-02-08 05:10:05 +01:00
|
|
|
collapse_key=Config.Bool.COLLAPSE_NOTETYPES,
|
2021-02-02 02:51:45 +01:00
|
|
|
type=SidebarItemType.NOTETYPE_ROOT,
|
2021-01-29 13:10:31 +01:00
|
|
|
)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
|
|
|
|
item = SidebarItem(
|
|
|
|
nt["name"],
|
2021-01-31 05:21:51 +01:00
|
|
|
icon,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=SearchNode(note=nt["name"]),
|
2021-01-23 10:59:12 +01:00
|
|
|
item_type=SidebarItemType.NOTETYPE,
|
|
|
|
id=nt["id"],
|
|
|
|
)
|
|
|
|
|
|
|
|
for c, tmpl in enumerate(nt["tmpls"]):
|
|
|
|
child = SidebarItem(
|
|
|
|
tmpl["name"],
|
2021-01-29 13:10:31 +01:00
|
|
|
icon,
|
2021-02-25 19:57:12 +01:00
|
|
|
search_node=self.col.group_searches(
|
2021-02-11 10:57:19 +01:00
|
|
|
SearchNode(note=nt["name"]), SearchNode(template=c)
|
2021-01-29 18:27:33 +01:00
|
|
|
),
|
2021-02-05 06:26:12 +01:00
|
|
|
item_type=SidebarItemType.NOTETYPE_TEMPLATE,
|
2021-02-11 01:09:06 +01:00
|
|
|
full_name=f"{nt['name']}::{tmpl['name']}",
|
2021-03-03 18:09:53 +01:00
|
|
|
id=tmpl["ord"],
|
2021-01-23 10:59:12 +01:00
|
|
|
)
|
2021-02-05 06:26:12 +01:00
|
|
|
item.add_child(child)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
root.add_child(item)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
|
|
|
# Context menu actions
|
|
|
|
###########################
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
def onContextMenu(self, point: QPoint) -> None:
|
|
|
|
idx: QModelIndex = self.indexAt(point)
|
2021-01-28 10:10:06 +01:00
|
|
|
item = self.model().item_for_index(idx)
|
2020-10-10 03:42:49 +02:00
|
|
|
if not item:
|
|
|
|
return
|
2021-01-29 13:50:29 +01:00
|
|
|
self.show_context_menu(item, idx)
|
2021-01-29 13:10:31 +01:00
|
|
|
|
2021-02-09 03:50:35 +01:00
|
|
|
# idx is only None when triggering the context menu from a left click on
|
|
|
|
# saved searches - perhaps there is a better way to handle that?
|
2021-03-03 21:57:39 +01:00
|
|
|
def show_context_menu(
|
|
|
|
self, item: SidebarItem, index: Optional[QModelIndex]
|
|
|
|
) -> None:
|
2021-01-29 13:50:29 +01:00
|
|
|
m = QMenu()
|
|
|
|
|
2021-03-04 18:31:35 +01:00
|
|
|
if item.item_type.is_deletable():
|
|
|
|
m.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(index))
|
2021-03-03 21:57:39 +01:00
|
|
|
if item.item_type.is_editable():
|
|
|
|
m.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index))
|
|
|
|
|
2021-01-29 13:50:29 +01:00
|
|
|
if item.item_type in self.context_menus:
|
|
|
|
for action in self.context_menus[item.item_type]:
|
|
|
|
act_name = action[0]
|
|
|
|
act_func = action[1]
|
|
|
|
a = m.addAction(act_name)
|
|
|
|
qconnect(a.triggered, lambda _, func=act_func: func(item))
|
|
|
|
|
2021-02-25 21:24:11 +01:00
|
|
|
self._maybe_add_search_actions(m)
|
2021-03-03 11:44:42 +01:00
|
|
|
self._maybe_add_tree_actions(m)
|
2021-01-29 13:50:29 +01:00
|
|
|
|
|
|
|
if not m.children():
|
2020-10-10 03:42:49 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
2021-02-25 21:24:11 +01:00
|
|
|
def _maybe_add_search_actions(self, menu: QMenu) -> None:
|
|
|
|
nodes = [
|
|
|
|
item.search_node for item in self._selected_items() if item.search_node
|
|
|
|
]
|
|
|
|
if not nodes:
|
|
|
|
return
|
|
|
|
menu.addSeparator()
|
|
|
|
if len(nodes) == 1:
|
|
|
|
menu.addAction(tr(TR.ACTIONS_SEARCH), lambda: self.update_search(*nodes))
|
|
|
|
return
|
|
|
|
sub_menu = menu.addMenu(tr(TR.ACTIONS_SEARCH))
|
|
|
|
sub_menu.addAction(
|
|
|
|
tr(TR.ACTIONS_ALL_SELECTED), lambda: self.update_search(*nodes)
|
|
|
|
)
|
|
|
|
sub_menu.addAction(
|
|
|
|
tr(TR.ACTIONS_ANY_SELECTED),
|
|
|
|
lambda: self.update_search(*nodes, joiner="OR"),
|
|
|
|
)
|
|
|
|
|
2021-03-03 11:44:42 +01:00
|
|
|
def _maybe_add_tree_actions(self, menu: QMenu) -> None:
|
|
|
|
def set_expanded(expanded: bool) -> None:
|
|
|
|
for index in self.selectedIndexes():
|
|
|
|
self.setExpanded(index, expanded)
|
|
|
|
|
|
|
|
def set_children_expanded(expanded: bool) -> None:
|
|
|
|
for index in self.selectedIndexes():
|
|
|
|
self.setExpanded(index, True)
|
|
|
|
for row in range(self.model().rowCount(index)):
|
|
|
|
self.setExpanded(self.model().index(row, 0, index), expanded)
|
|
|
|
|
2021-01-29 13:50:29 +01:00
|
|
|
if self.current_search:
|
|
|
|
return
|
|
|
|
|
2021-03-03 11:44:42 +01:00
|
|
|
selected_items = self._selected_items()
|
|
|
|
if not any(item.children for item in selected_items):
|
|
|
|
return
|
2021-01-29 13:50:29 +01:00
|
|
|
|
|
|
|
menu.addSeparator()
|
2021-03-05 10:27:44 +01:00
|
|
|
if any(not item.expanded for item in selected_items if item.children):
|
2021-03-03 11:44:42 +01:00
|
|
|
menu.addAction(tr(TR.BROWSING_SIDEBAR_EXPAND), lambda: set_expanded(True))
|
2021-03-05 10:27:44 +01:00
|
|
|
if any(item.expanded for item in selected_items if item.children):
|
2021-03-03 11:44:42 +01:00
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_SIDEBAR_COLLAPSE), lambda: set_expanded(False)
|
|
|
|
)
|
|
|
|
if any(
|
|
|
|
not c.expanded for i in selected_items for c in i.children if c.children
|
|
|
|
):
|
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_SIDEBAR_EXPAND_CHILDREN),
|
|
|
|
lambda: set_children_expanded(True),
|
|
|
|
)
|
|
|
|
if any(c.expanded for i in selected_items for c in i.children if c.children):
|
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_SIDEBAR_COLLAPSE_CHILDREN),
|
|
|
|
lambda: set_children_expanded(False),
|
|
|
|
)
|
2021-01-29 13:50:29 +01:00
|
|
|
|
2021-03-03 21:57:39 +01:00
|
|
|
def rename_deck(self, item: SidebarItem, new_name: str) -> None:
|
2020-10-10 03:42:49 +02:00
|
|
|
deck = self.mw.col.decks.get(item.id)
|
|
|
|
old_name = deck["name"]
|
2021-03-03 21:57:39 +01:00
|
|
|
new_name = re.sub(
|
|
|
|
re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name
|
|
|
|
)
|
2021-01-03 08:30:14 +01:00
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK))
|
2020-10-10 03:42:49 +02:00
|
|
|
try:
|
|
|
|
self.mw.col.decks.rename(deck, new_name)
|
|
|
|
except DeckRenameError as e:
|
2021-02-01 11:23:48 +01:00
|
|
|
showWarning(e.description)
|
2021-02-28 22:36:21 +01:00
|
|
|
return
|
2021-03-02 23:13:34 +01:00
|
|
|
self.refresh(
|
2021-03-03 18:09:53 +01:00
|
|
|
lambda other: other.item_type == SidebarItemType.DECK
|
|
|
|
and other.id == item.id
|
2021-03-02 23:13:34 +01:00
|
|
|
)
|
2021-01-03 01:09:42 +01:00
|
|
|
self.mw.deckBrowser.refresh()
|
2021-01-03 08:36:54 +01:00
|
|
|
|
2021-03-02 11:05:16 +01:00
|
|
|
def remove_tags(self, item: SidebarItem) -> None:
|
|
|
|
self.browser.editor.saveNow(lambda: self._remove_tags(item))
|
2021-01-06 15:04:03 +01:00
|
|
|
|
2021-03-02 11:05:16 +01:00
|
|
|
def _remove_tags(self, _item: SidebarItem) -> None:
|
|
|
|
tags = self._selected_tags()
|
2021-01-06 15:04:03 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def do_remove() -> None:
|
2021-03-02 11:05:16 +01:00
|
|
|
self.col._backend.expunge_tags(" ".join(tags))
|
2021-01-06 15:04:03 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def on_done(fut: Future) -> None:
|
2021-01-06 15:04:03 +01:00
|
|
|
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
|
|
|
self.browser.model.endReset()
|
|
|
|
fut.result()
|
2021-01-29 02:20:15 +01:00
|
|
|
self.refresh()
|
2021-01-06 15:04:03 +01:00
|
|
|
|
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_REMOVE_TAG))
|
|
|
|
self.browser.model.beginReset()
|
|
|
|
self.mw.taskman.run_in_background(do_remove, on_done)
|
|
|
|
|
2021-03-03 21:57:39 +01:00
|
|
|
def rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
|
|
|
new_name = new_name.replace(" ", "")
|
|
|
|
if new_name and new_name != item.name:
|
|
|
|
# block repainting until collection is updated
|
2021-03-01 08:45:33 +01:00
|
|
|
self.setUpdatesEnabled(False)
|
2021-03-03 21:57:39 +01:00
|
|
|
self.browser.editor.saveNow(lambda: self._rename_tag(item, new_name))
|
2021-01-03 08:36:54 +01:00
|
|
|
|
2021-03-03 21:57:39 +01:00
|
|
|
def _rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
2021-01-09 02:49:10 +01:00
|
|
|
old_name = item.full_name
|
2021-03-03 21:57:39 +01:00
|
|
|
new_name = re.sub(
|
|
|
|
re.escape(item.name) + "$", new_name.replace("\\", r"\\"), old_name
|
|
|
|
)
|
2021-01-04 05:13:20 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def do_rename() -> int:
|
2021-01-31 09:46:43 +01:00
|
|
|
self.mw.col.tags.remove(old_name)
|
|
|
|
return self.col.tags.rename(old_name, new_name)
|
2021-01-04 05:13:20 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def on_done(fut: Future) -> None:
|
2021-01-04 05:13:20 +01:00
|
|
|
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
|
|
|
self.browser.model.endReset()
|
|
|
|
|
|
|
|
count = fut.result()
|
|
|
|
if not count:
|
|
|
|
showInfo(tr(TR.BROWSING_TAG_RENAME_WARNING_EMPTY))
|
|
|
|
return
|
|
|
|
|
2021-03-02 23:13:34 +01:00
|
|
|
self.refresh(
|
|
|
|
lambda item: item.item_type == SidebarItemType.TAG
|
|
|
|
and item.full_name == new_name
|
|
|
|
)
|
2021-01-04 05:13:20 +01:00
|
|
|
|
2021-01-03 08:36:54 +01:00
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME_TAG))
|
|
|
|
self.browser.model.beginReset()
|
2021-01-04 05:13:20 +01:00
|
|
|
self.mw.taskman.run_in_background(do_rename, on_done)
|
2021-01-04 11:30:41 +01:00
|
|
|
|
2021-03-04 18:31:35 +01:00
|
|
|
def delete_decks(self, _item: SidebarItem) -> None:
|
2021-02-26 19:52:34 +01:00
|
|
|
self.browser.editor.saveNow(self._delete_decks)
|
2021-01-04 11:30:41 +01:00
|
|
|
|
2021-02-26 19:52:34 +01:00
|
|
|
def _delete_decks(self) -> None:
|
|
|
|
dids = self._selected_decks()
|
|
|
|
if self.mw.deckBrowser.ask_delete_decks(dids):
|
2021-01-04 11:30:41 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def do_delete() -> None:
|
2021-02-26 19:52:34 +01:00
|
|
|
return self.mw.col.decks.remove(dids)
|
2021-01-04 11:30:41 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def on_done(fut: Future) -> None:
|
2021-01-04 11:36:45 +01:00
|
|
|
self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self)
|
2021-01-04 11:30:41 +01:00
|
|
|
self.browser.search()
|
|
|
|
self.browser.model.endReset()
|
2021-01-29 02:20:15 +01:00
|
|
|
self.refresh()
|
2021-01-04 11:30:41 +01:00
|
|
|
res = fut.result() # Required to check for errors
|
|
|
|
|
|
|
|
self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK))
|
|
|
|
self.browser.model.beginReset()
|
|
|
|
self.mw.taskman.run_in_background(do_delete, on_done)
|
2021-01-20 01:26:53 +01:00
|
|
|
|
2021-03-03 15:18:50 +01:00
|
|
|
def rename_notetype(self, item: SidebarItem, new_name: str) -> None:
|
|
|
|
notetype = self.col.models.get(item.id)
|
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME))
|
|
|
|
notetype["name"] = new_name
|
|
|
|
self.col.models.save(notetype)
|
|
|
|
self.refresh(
|
2021-03-03 18:09:53 +01:00
|
|
|
lambda other: other.item_type == SidebarItemType.NOTETYPE
|
|
|
|
and other.id == item.id
|
2021-03-03 15:18:50 +01:00
|
|
|
)
|
2021-03-03 18:09:53 +01:00
|
|
|
self.browser.model.reset()
|
|
|
|
|
|
|
|
def rename_template(self, item: SidebarItem, new_name: str) -> None:
|
|
|
|
notetype = self.col.models.get(item._parent_item.id)
|
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME))
|
2021-03-03 21:57:39 +01:00
|
|
|
notetype["tmpls"][item.id]["name"] = new_name
|
2021-03-03 18:09:53 +01:00
|
|
|
self.col.models.save(notetype)
|
|
|
|
self.refresh(
|
|
|
|
lambda other: other.item_type == SidebarItemType.NOTETYPE_TEMPLATE
|
|
|
|
and other._parent_item.id == item._parent_item.id
|
|
|
|
and other.id == item.id
|
|
|
|
)
|
|
|
|
self.browser.model.reset()
|
2021-03-03 15:18:50 +01:00
|
|
|
|
2021-02-28 21:03:19 +01:00
|
|
|
def rename_node(self, item: SidebarItem, text: str) -> bool:
|
2021-03-03 21:57:39 +01:00
|
|
|
new_name = text.replace('"', "")
|
|
|
|
if new_name and new_name != item.name:
|
2021-02-28 21:03:19 +01:00
|
|
|
if item.item_type == SidebarItemType.DECK:
|
2021-03-03 21:57:39 +01:00
|
|
|
self.rename_deck(item, new_name)
|
2021-03-03 18:09:53 +01:00
|
|
|
elif item.item_type == SidebarItemType.SAVED_SEARCH:
|
2021-03-03 21:57:39 +01:00
|
|
|
self.rename_saved_search(item, new_name)
|
2021-03-03 18:09:53 +01:00
|
|
|
elif item.item_type == SidebarItemType.TAG:
|
2021-03-03 21:57:39 +01:00
|
|
|
self.rename_tag(item, new_name)
|
2021-03-03 18:09:53 +01:00
|
|
|
elif item.item_type == SidebarItemType.NOTETYPE:
|
2021-03-03 21:57:39 +01:00
|
|
|
self.rename_notetype(item, new_name)
|
2021-03-03 18:09:53 +01:00
|
|
|
elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE:
|
2021-03-03 21:57:39 +01:00
|
|
|
self.rename_template(item, new_name)
|
2021-02-28 22:36:21 +01:00
|
|
|
# renaming may be asynchronous so always return False
|
2021-02-28 21:03:19 +01:00
|
|
|
return False
|
|
|
|
|
2021-02-05 13:38:44 +01:00
|
|
|
# Saved searches
|
|
|
|
##################
|
|
|
|
|
|
|
|
_saved_searches_key = "savedFilters"
|
|
|
|
|
|
|
|
def _get_saved_searches(self) -> Dict[str, str]:
|
|
|
|
return self.col.get_config(self._saved_searches_key, {})
|
|
|
|
|
|
|
|
def _set_saved_searches(self, searches: Dict[str, str]) -> None:
|
|
|
|
self.col.set_config(self._saved_searches_key, searches)
|
|
|
|
|
2021-03-01 09:41:41 +01:00
|
|
|
def remove_saved_searches(self, _item: SidebarItem) -> None:
|
|
|
|
selected = self._selected_saved_searches()
|
|
|
|
if len(selected) == 1:
|
|
|
|
query = tr(TR.BROWSING_REMOVE_FROM_YOUR_SAVED_SEARCHES, val=selected[0])
|
|
|
|
else:
|
|
|
|
query = tr(TR.BROWSING_CONFIRM_SAVED_SEARCHES_DELETION, count=len(selected))
|
|
|
|
if not askUser(query):
|
2021-01-29 14:05:30 +01:00
|
|
|
return
|
2021-02-05 13:38:44 +01:00
|
|
|
conf = self._get_saved_searches()
|
2021-03-01 09:41:41 +01:00
|
|
|
for name in selected:
|
|
|
|
del conf[name]
|
2021-02-05 13:38:44 +01:00
|
|
|
self._set_saved_searches(conf)
|
2021-01-29 14:05:30 +01:00
|
|
|
self.refresh()
|
2021-01-20 01:26:53 +01:00
|
|
|
|
2021-03-03 21:57:39 +01:00
|
|
|
def rename_saved_search(self, item: SidebarItem, new_name: str) -> None:
|
|
|
|
old_name = item.name
|
2021-02-05 13:38:44 +01:00
|
|
|
conf = self._get_saved_searches()
|
2021-01-29 14:05:30 +01:00
|
|
|
try:
|
2021-03-03 21:57:39 +01:00
|
|
|
filt = conf[old_name]
|
2021-01-29 14:05:30 +01:00
|
|
|
except KeyError:
|
2021-02-28 22:36:21 +01:00
|
|
|
return
|
2021-03-04 17:22:03 +01:00
|
|
|
if new_name in conf and not askUser(
|
|
|
|
tr(TR.BROWSING_CONFIRM_SAVED_SEARCH_OVERWRITE, name=new_name)
|
|
|
|
):
|
|
|
|
return
|
2021-03-03 21:57:39 +01:00
|
|
|
conf[new_name] = filt
|
|
|
|
del conf[old_name]
|
2021-02-05 13:38:44 +01:00
|
|
|
self._set_saved_searches(conf)
|
2021-03-02 23:13:34 +01:00
|
|
|
self.refresh(
|
|
|
|
lambda item: item.item_type == SidebarItemType.SAVED_SEARCH
|
|
|
|
and item.name == new_name
|
|
|
|
)
|
2021-01-29 14:05:30 +01:00
|
|
|
|
2021-02-01 00:51:46 +01:00
|
|
|
def save_current_search(self, _item: Any = None) -> None:
|
2021-01-29 14:05:30 +01:00
|
|
|
try:
|
2021-01-31 05:21:51 +01:00
|
|
|
filt = self.col.build_search_string(
|
2021-01-29 14:05:30 +01:00
|
|
|
self.browser.form.searchEdit.lineEdit().text()
|
|
|
|
)
|
|
|
|
except InvalidInput as e:
|
|
|
|
show_invalid_search_error(e)
|
2021-03-04 17:22:03 +01:00
|
|
|
return
|
|
|
|
name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME))
|
|
|
|
if not name:
|
|
|
|
return
|
|
|
|
conf = self._get_saved_searches()
|
|
|
|
if name in conf and not askUser(
|
|
|
|
tr(TR.BROWSING_CONFIRM_SAVED_SEARCH_OVERWRITE, name=name)
|
|
|
|
):
|
|
|
|
return
|
|
|
|
conf[name] = filt
|
|
|
|
self._set_saved_searches(conf)
|
|
|
|
self.refresh(
|
|
|
|
lambda item: item.item_type == SidebarItemType.SAVED_SEARCH
|
|
|
|
and item.name == name
|
|
|
|
)
|
2021-01-20 00:00:53 +01:00
|
|
|
|
2021-02-05 06:26:12 +01:00
|
|
|
def manage_notetype(self, item: SidebarItem) -> None:
|
2021-01-22 03:56:39 +01:00
|
|
|
Models(
|
|
|
|
self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id
|
|
|
|
)
|
2021-02-25 21:24:11 +01:00
|
|
|
|
|
|
|
# Helpers
|
|
|
|
##################
|
|
|
|
|
|
|
|
def _selected_items(self) -> List[SidebarItem]:
|
|
|
|
return [self.model().item_for_index(idx) for idx in self.selectedIndexes()]
|
2021-02-26 19:52:34 +01:00
|
|
|
|
|
|
|
def _selected_decks(self) -> List[int]:
|
|
|
|
return [
|
|
|
|
item.id
|
|
|
|
for item in self._selected_items()
|
|
|
|
if item.item_type == SidebarItemType.DECK
|
|
|
|
]
|
2021-03-01 09:41:41 +01:00
|
|
|
|
|
|
|
def _selected_saved_searches(self) -> List[str]:
|
|
|
|
return [
|
|
|
|
item.name
|
|
|
|
for item in self._selected_items()
|
|
|
|
if item.item_type == SidebarItemType.SAVED_SEARCH
|
|
|
|
]
|
2021-03-02 11:05:16 +01:00
|
|
|
|
|
|
|
def _selected_tags(self) -> List[str]:
|
|
|
|
return [
|
|
|
|
item.full_name
|
|
|
|
for item in self._selected_items()
|
|
|
|
if item.item_type == SidebarItemType.TAG
|
|
|
|
]
|