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-05 06:26:12 +01:00
|
|
|
from enum import Enum, auto
|
2021-03-05 12:22:49 +01:00
|
|
|
from typing import Dict, Iterable, List, Optional, Tuple, cast
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
import aqt
|
2021-03-16 10:21:18 +01:00
|
|
|
from anki.collection import Config, OpChanges, SearchJoiner, SearchNode
|
2021-03-22 14:43:54 +01:00
|
|
|
from anki.decks import DeckID, DeckTreeNode
|
2021-03-10 10:14:06 +01:00
|
|
|
from anki.notes import Note
|
2021-01-31 06:55:08 +01:00
|
|
|
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-03-10 10:14:06 +01:00
|
|
|
from aqt.clayout import CardLayout
|
2021-03-22 11:38:51 +01:00
|
|
|
from aqt.deck_ops import remove_decks, rename_deck, reparent_decks
|
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-03-19 10:15:17 +01:00
|
|
|
from aqt.tag_ops import remove_tags_for_all_notes, rename_tag, reparent_tags
|
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,
|
2021-03-17 05:51:59 +01:00
|
|
|
KeyboardModifiersPressed,
|
2021-01-29 14:05:30 +01:00
|
|
|
askUser,
|
|
|
|
getOnlyText,
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
showWarning,
|
2021-01-29 14:05:30 +01:00
|
|
|
tr,
|
|
|
|
)
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
|
2021-02-25 11:03:57 +01:00
|
|
|
class SidebarTool(Enum):
|
|
|
|
SELECT = auto()
|
|
|
|
SEARCH = auto()
|
|
|
|
|
|
|
|
|
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-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,
|
2021-03-10 16:38:29 +01:00
|
|
|
name_prefix: str = "",
|
2021-01-23 10:59:12 +01:00
|
|
|
) -> None:
|
|
|
|
self.name = name
|
2021-03-10 16:38:29 +01:00
|
|
|
self.name_prefix = name_prefix
|
|
|
|
self.full_name = name_prefix + name
|
2021-01-23 10:59:12 +01:00
|
|
|
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-03-17 05:51:59 +01:00
|
|
|
def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool:
|
2021-03-13 09:45:06 +01:00
|
|
|
return self.sidebar._on_rename(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-03-08 11:35:39 +01:00
|
|
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
2021-01-30 12:32:25 +01:00
|
|
|
item: SidebarItem = index.internalPointer()
|
2021-03-08 11:35:39 +01:00
|
|
|
if item.item_type in self.sidebar.valid_drop_types:
|
|
|
|
flags |= 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-03-08 11:55:15 +01:00
|
|
|
_tools: Tuple[Tuple[SidebarTool, str, TR.V], ...] = (
|
|
|
|
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", TR.ACTIONS_SEARCH),
|
|
|
|
(SidebarTool.SELECT, ":/icons/select.svg", TR.ACTIONS_SELECT),
|
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-03-09 08:50:01 +01:00
|
|
|
self.setIconSize(QSize(16, 16))
|
|
|
|
self.setStyle(QStyleFactory.create("fusion"))
|
2021-02-25 11:03:57 +01:00
|
|
|
|
2021-02-25 11:35:31 +01:00
|
|
|
def _setup_tools(self) -> None:
|
2021-03-09 11:19:44 +01:00
|
|
|
for row, tool in enumerate(self._tools):
|
2021-03-08 11:55:15 +01:00
|
|
|
action = self.addAction(
|
2021-03-09 11:19:44 +01:00
|
|
|
theme_manager.icon_from_resources(tool[1]), tr(tool[2])
|
2021-03-08 11:55:15 +01:00
|
|
|
)
|
2021-02-25 11:03:57 +01:00
|
|
|
action.setCheckable(True)
|
2021-03-09 11:19:44 +01:00
|
|
|
action.setShortcut(f"Alt+{row + 1}")
|
2021-02-25 11:03:57 +01:00
|
|
|
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-03-17 05:51:59 +01:00
|
|
|
# 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
|
|
|
|
# search bar which is wrong, and the layout code is handled in browser.py instead
|
|
|
|
# of here
|
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-03-08 11:35:39 +01:00
|
|
|
self.valid_drop_types: Tuple[SidebarItemType, ...] = ()
|
2021-03-16 10:21:18 +01:00
|
|
|
self._refresh_needed = False
|
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-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-03-08 11:55:15 +01:00
|
|
|
self.setEditTriggers(QAbstractItemView.EditKeyPressed)
|
2021-01-30 12:08:39 +01:00
|
|
|
|
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-03-17 05:51:59 +01:00
|
|
|
# these do not really belong here, they should be in a higher-level class
|
|
|
|
self.toolbar = SidebarToolbar(self)
|
|
|
|
self.searchBar = SidebarSearchBar(self)
|
|
|
|
|
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
|
2021-03-08 11:55:15 +01:00
|
|
|
if 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-03-09 20:18:12 +01:00
|
|
|
double_click_expands = False
|
2021-03-08 11:55:15 +01:00
|
|
|
else:
|
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-03-09 20:18:12 +01:00
|
|
|
double_click_expands = True
|
2021-02-25 13:12:51 +01:00
|
|
|
self.setSelectionMode(selection_mode)
|
|
|
|
self.setDragDropMode(drag_drop_mode)
|
2021-03-09 20:18:12 +01:00
|
|
|
self.setExpandsOnDoubleClick(double_click_expands)
|
2021-02-25 11:06:59 +01:00
|
|
|
|
2021-02-02 01:40:50 +01:00
|
|
|
def model(self) -> SidebarModel:
|
2021-03-17 05:51:59 +01:00
|
|
|
return cast(SidebarModel, super().model())
|
2021-01-28 10:10:06 +01:00
|
|
|
|
2021-03-16 10:21:18 +01:00
|
|
|
# Refreshing
|
|
|
|
###########################
|
|
|
|
|
|
|
|
def op_executed(self, op: OpChanges, focused: bool) -> None:
|
|
|
|
if op.tag or op.notetype or op.deck:
|
|
|
|
self._refresh_needed = True
|
|
|
|
if focused:
|
|
|
|
self.refresh_if_needed()
|
|
|
|
|
|
|
|
def refresh_if_needed(self) -> None:
|
|
|
|
if self._refresh_needed:
|
|
|
|
self.refresh()
|
|
|
|
self._refresh_needed = False
|
|
|
|
|
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
|
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
def on_done(root: SidebarItem) -> None:
|
|
|
|
# user may have closed browser
|
|
|
|
if sip.isdeleted(self):
|
|
|
|
return
|
2021-01-28 08:47:58 +01:00
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
# block repainting during refreshing to avoid flickering
|
|
|
|
self.setUpdatesEnabled(False)
|
2021-01-28 08:47:58 +01:00
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
model = SidebarModel(self, root)
|
2021-01-28 09:51:18 +01:00
|
|
|
self.setModel(model)
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
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-03-02 23:13:34 +01:00
|
|
|
if is_current:
|
|
|
|
self.restore_current(is_current)
|
2021-01-26 00:02:08 +01:00
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
self.setUpdatesEnabled(True)
|
|
|
|
|
|
|
|
# needs to be set after changing model
|
|
|
|
qconnect(self.selectionModel().selectionChanged, self._on_selection_changed)
|
|
|
|
|
|
|
|
self.mw.query_op(self._root_tree, success=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."""
|
2021-03-17 05:51:59 +01:00
|
|
|
mods = KeyboardModifiersPressed()
|
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
|
2021-03-17 05:51:59 +01:00
|
|
|
if mods.alt:
|
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:
|
2021-03-17 05:51:59 +01:00
|
|
|
if mods.control and mods.shift:
|
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-03-17 05:51:59 +01:00
|
|
|
elif mods.control:
|
2021-02-11 08:11:17 +01:00
|
|
|
# If Ctrl, AND with previous
|
|
|
|
search = self.col.join_searches(previous, current, "AND")
|
2021-03-17 05:51:59 +01:00
|
|
|
elif mods.shift:
|
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)
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
except Exception as e:
|
|
|
|
showWarning(str(e))
|
2021-02-09 01:50:39 +01:00
|
|
|
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-03-09 20:36:15 +01:00
|
|
|
if (index := self.currentIndex()) == self.indexAt(event.pos()):
|
|
|
|
self._on_search(index)
|
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:
|
2021-03-13 09:31:56 +01:00
|
|
|
self._on_delete_key(index)
|
2021-02-05 06:26:12 +01:00
|
|
|
else:
|
|
|
|
super().keyPressEvent(event)
|
|
|
|
|
2021-03-13 09:45:06 +01:00
|
|
|
# Slots
|
2021-02-05 06:26:12 +01:00
|
|
|
###########
|
|
|
|
|
2021-03-08 11:35:39 +01:00
|
|
|
def _on_selection_changed(self, _new: QItemSelection, _old: QItemSelection) -> None:
|
|
|
|
selected_types = [item.item_type for item in self._selected_items()]
|
|
|
|
if all(item_type == SidebarItemType.DECK for item_type in selected_types):
|
|
|
|
self.valid_drop_types = (SidebarItemType.DECK, SidebarItemType.DECK_ROOT)
|
|
|
|
elif all(item_type == SidebarItemType.TAG for item_type in selected_types):
|
|
|
|
self.valid_drop_types = (SidebarItemType.TAG, SidebarItemType.TAG_ROOT)
|
|
|
|
else:
|
|
|
|
self.valid_drop_types = ()
|
|
|
|
|
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:
|
2021-03-22 09:23:56 +01:00
|
|
|
deck_ids = [
|
2021-03-22 14:43:54 +01:00
|
|
|
DeckID(source.id)
|
|
|
|
for source in sources
|
|
|
|
if source.item_type == SidebarItemType.DECK
|
2021-01-30 12:32:25 +01:00
|
|
|
]
|
2021-03-22 09:23:56 +01:00
|
|
|
if not deck_ids:
|
2021-01-30 12:08:39 +01:00
|
|
|
return False
|
|
|
|
|
2021-03-22 14:43:54 +01:00
|
|
|
new_parent = DeckID(target.id)
|
2021-01-30 12:08:39 +01:00
|
|
|
|
2021-03-22 09:23:56 +01:00
|
|
|
reparent_decks(
|
|
|
|
mw=self.mw, parent=self.browser, deck_ids=deck_ids, new_parent=new_parent
|
|
|
|
)
|
2021-02-02 12:45:54 +01:00
|
|
|
|
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:
|
2021-03-19 10:15:17 +01:00
|
|
|
tags = [
|
2021-02-02 11:14:04 +01:00
|
|
|
source.full_name
|
|
|
|
for source in sources
|
|
|
|
if source.item_type == SidebarItemType.TAG
|
|
|
|
]
|
2021-03-19 10:15:17 +01:00
|
|
|
if not tags:
|
2021-02-02 11:14:04 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
if target.item_type == SidebarItemType.TAG_ROOT:
|
2021-03-19 10:15:17 +01:00
|
|
|
new_parent = ""
|
2021-02-02 11:14:04 +01:00
|
|
|
else:
|
2021-03-19 10:15:17 +01:00
|
|
|
new_parent = target.full_name
|
2021-02-02 11:14:04 +01:00
|
|
|
|
2021-03-19 10:15:17 +01:00
|
|
|
reparent_tags(mw=self.mw, parent=self.browser, tags=tags, new_parent=new_parent)
|
2021-02-02 12:45:54 +01:00
|
|
|
|
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-13 09:45:06 +01:00
|
|
|
def _on_rename(self, item: SidebarItem, text: str) -> bool:
|
|
|
|
new_name = text.replace('"', "")
|
|
|
|
if new_name and new_name != item.name:
|
|
|
|
if item.item_type == SidebarItemType.DECK:
|
|
|
|
self.rename_deck(item, new_name)
|
|
|
|
elif item.item_type == SidebarItemType.SAVED_SEARCH:
|
|
|
|
self.rename_saved_search(item, new_name)
|
|
|
|
elif item.item_type == SidebarItemType.TAG:
|
|
|
|
self.rename_tag(item, new_name)
|
|
|
|
# renaming may be asynchronous so always return False
|
|
|
|
return False
|
|
|
|
|
2021-03-13 09:31:56 +01:00
|
|
|
def _on_delete_key(self, index: QModelIndex) -> None:
|
2021-03-04 18:31:35 +01:00
|
|
|
if item := self.model().item_for_index(index):
|
2021-03-13 09:31:56 +01:00
|
|
|
if self._enable_delete(item):
|
|
|
|
self._on_delete(item)
|
|
|
|
|
|
|
|
def _enable_delete(self, item: SidebarItem) -> bool:
|
|
|
|
return item.item_type.is_deletable() and all(
|
|
|
|
s.item_type == item.item_type for s in self._selected_items()
|
|
|
|
)
|
|
|
|
|
|
|
|
def _on_delete(self, item: SidebarItem) -> None:
|
|
|
|
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-03-04 18:31:35 +01:00
|
|
|
|
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-03-17 05:54:06 +01:00
|
|
|
handled = gui_hooks.browser_will_build_tree(
|
|
|
|
False, root, stage, self.browser
|
|
|
|
)
|
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,
|
2021-03-10 16:38:29 +01:00
|
|
|
name_prefix=head,
|
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-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-03-10 16:38:29 +01:00
|
|
|
name_prefix=head,
|
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-03-10 16:38:29 +01:00
|
|
|
name_prefix=f"{nt['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
|
|
|
|
2021-03-13 09:45:06 +01:00
|
|
|
# Context menu
|
2021-01-23 10:59:12 +01:00
|
|
|
###########################
|
2020-10-10 03:42:49 +02:00
|
|
|
|
|
|
|
def onContextMenu(self, point: QPoint) -> None:
|
2021-03-07 10:30:20 +01:00
|
|
|
index: QModelIndex = self.indexAt(point)
|
|
|
|
item = self.model().item_for_index(index)
|
|
|
|
if item and self.selectionModel().isSelected(index):
|
|
|
|
self.show_context_menu(item, index)
|
|
|
|
|
|
|
|
def show_context_menu(self, item: SidebarItem, index: QModelIndex) -> None:
|
|
|
|
menu = QMenu()
|
|
|
|
self._maybe_add_type_specific_actions(menu, item)
|
|
|
|
self._maybe_add_delete_action(menu, item, index)
|
|
|
|
self._maybe_add_rename_action(menu, item, index)
|
|
|
|
self._maybe_add_search_actions(menu)
|
|
|
|
self._maybe_add_tree_actions(menu)
|
|
|
|
if menu.children():
|
|
|
|
menu.exec_(QCursor.pos())
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-03-05 12:22:49 +01:00
|
|
|
def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarItem) -> None:
|
|
|
|
if item.item_type in (SidebarItemType.NOTETYPE, SidebarItemType.NOTETYPE_ROOT):
|
2021-03-10 10:14:06 +01:00
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_MANAGE_NOTE_TYPES), lambda: self.manage_notetype(item)
|
|
|
|
)
|
|
|
|
elif item.item_type == SidebarItemType.NOTETYPE_TEMPLATE:
|
|
|
|
menu.addAction(tr(TR.NOTETYPES_CARDS), lambda: self.manage_template(item))
|
2021-03-05 12:22:49 +01:00
|
|
|
elif item.item_type == SidebarItemType.SAVED_SEARCH_ROOT:
|
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_SIDEBAR_SAVE_CURRENT_SEARCH), self.save_current_search
|
|
|
|
)
|
2021-03-23 11:20:46 +01:00
|
|
|
elif item.item_type == SidebarItemType.SAVED_SEARCH:
|
|
|
|
menu.addAction(
|
|
|
|
tr(TR.BROWSING_UPDATE_SAVED_SEARCH),
|
|
|
|
lambda: self.update_saved_search(item),
|
|
|
|
)
|
2021-03-05 12:22:49 +01:00
|
|
|
|
2021-03-07 10:30:20 +01:00
|
|
|
def _maybe_add_delete_action(
|
|
|
|
self, menu: QMenu, item: SidebarItem, index: QModelIndex
|
|
|
|
) -> None:
|
2021-03-13 09:31:56 +01:00
|
|
|
if self._enable_delete(item):
|
|
|
|
menu.addAction(tr(TR.ACTIONS_DELETE), lambda: self._on_delete(item))
|
2021-03-07 10:30:20 +01:00
|
|
|
|
|
|
|
def _maybe_add_rename_action(
|
|
|
|
self, menu: QMenu, item: SidebarItem, index: QModelIndex
|
|
|
|
) -> None:
|
|
|
|
if item.item_type.is_editable() and len(self._selected_items()) == 1:
|
|
|
|
menu.addAction(tr(TR.ACTIONS_RENAME), lambda: self.edit(index))
|
|
|
|
|
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-13 09:45:06 +01:00
|
|
|
# Decks
|
|
|
|
###########################
|
|
|
|
|
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)
|
2021-03-22 11:38:51 +01:00
|
|
|
if not new_name:
|
|
|
|
return
|
2021-03-10 16:38:29 +01:00
|
|
|
new_name = item.name_prefix + new_name
|
2021-03-22 11:38:51 +01:00
|
|
|
if new_name == deck["name"]:
|
2021-02-28 22:36:21 +01:00
|
|
|
return
|
2021-03-22 11:38:51 +01:00
|
|
|
|
|
|
|
rename_deck(
|
|
|
|
mw=self.mw,
|
2021-03-22 14:43:54 +01:00
|
|
|
deck_id=DeckID(item.id),
|
2021-03-22 11:38:51 +01:00
|
|
|
new_name=new_name,
|
|
|
|
after_rename=lambda: self.refresh(
|
|
|
|
lambda other: other.item_type == SidebarItemType.DECK
|
|
|
|
and other.id == item.id
|
|
|
|
),
|
2021-03-02 23:13:34 +01:00
|
|
|
)
|
2021-01-03 08:36:54 +01:00
|
|
|
|
2021-03-13 09:45:06 +01:00
|
|
|
def delete_decks(self, _item: SidebarItem) -> None:
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
remove_decks(mw=self.mw, parent=self.browser, deck_ids=self._selected_decks())
|
2021-03-13 09:45:06 +01:00
|
|
|
|
|
|
|
# Tags
|
|
|
|
###########################
|
|
|
|
|
2021-03-18 12:35:32 +01:00
|
|
|
def remove_tags(self, item: SidebarItem) -> None:
|
|
|
|
tags = self.mw.col.tags.join(self._selected_tags())
|
|
|
|
item.name = "..."
|
2021-01-06 15:04:03 +01:00
|
|
|
|
2021-03-18 12:35:32 +01:00
|
|
|
remove_tags_for_all_notes(
|
|
|
|
mw=self.mw, parent=self.browser, space_separated_tags=tags
|
|
|
|
)
|
2021-01-06 15:04:03 +01:00
|
|
|
|
2021-03-03 21:57:39 +01:00
|
|
|
def rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
2021-03-18 11:43:04 +01:00
|
|
|
if not new_name or new_name == item.name:
|
|
|
|
return
|
|
|
|
|
|
|
|
new_name_base = new_name
|
2021-01-03 08:36:54 +01:00
|
|
|
|
2021-01-09 02:49:10 +01:00
|
|
|
old_name = item.full_name
|
2021-03-10 16:38:29 +01:00
|
|
|
new_name = item.name_prefix + new_name
|
2021-01-04 05:13:20 +01:00
|
|
|
|
2021-03-18 11:43:04 +01:00
|
|
|
item.name = new_name_base
|
2021-01-04 05:13:20 +01:00
|
|
|
|
2021-03-18 11:43:04 +01:00
|
|
|
rename_tag(
|
|
|
|
mw=self.mw,
|
|
|
|
parent=self.browser,
|
|
|
|
current_name=old_name,
|
|
|
|
new_name=new_name,
|
|
|
|
after_rename=lambda: self.refresh(
|
|
|
|
lambda item: item.item_type == SidebarItemType.TAG
|
|
|
|
and item.full_name == new_name
|
|
|
|
),
|
|
|
|
)
|
2021-01-04 11:30:41 +01:00
|
|
|
|
2021-02-05 13:38:44 +01:00
|
|
|
# Saved searches
|
2021-03-13 09:45:06 +01:00
|
|
|
####################################
|
2021-02-05 13:38:44 +01:00
|
|
|
|
|
|
|
_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-23 11:20:46 +01:00
|
|
|
def _get_current_search(self) -> Optional[str]:
|
|
|
|
try:
|
|
|
|
return self.col.build_search_string(
|
|
|
|
self.browser.form.searchEdit.lineEdit().text()
|
|
|
|
)
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
except Exception as e:
|
|
|
|
showWarning(str(e))
|
2021-03-23 11:20:46 +01:00
|
|
|
return None
|
|
|
|
|
|
|
|
def _save_search(self, name: str, search: str, update: bool = False) -> None:
|
|
|
|
conf = self._get_saved_searches()
|
|
|
|
if (
|
|
|
|
not update
|
|
|
|
and name in conf
|
|
|
|
and not askUser(tr(TR.BROWSING_CONFIRM_SAVED_SEARCH_OVERWRITE, name=name))
|
|
|
|
):
|
|
|
|
return
|
|
|
|
conf[name] = search
|
|
|
|
self._set_saved_searches(conf)
|
|
|
|
self.refresh(
|
|
|
|
lambda item: item.item_type == SidebarItemType.SAVED_SEARCH
|
|
|
|
and item.name == name
|
|
|
|
)
|
|
|
|
|
2021-03-01 09:41:41 +01:00
|
|
|
def remove_saved_searches(self, _item: SidebarItem) -> None:
|
|
|
|
selected = self._selected_saved_searches()
|
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-03-05 12:22:49 +01:00
|
|
|
def save_current_search(self) -> None:
|
2021-03-23 11:20:46 +01:00
|
|
|
if (search := self._get_current_search()) is None:
|
2021-03-04 17:22:03 +01:00
|
|
|
return
|
|
|
|
name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME))
|
|
|
|
if not name:
|
|
|
|
return
|
2021-03-23 11:20:46 +01:00
|
|
|
self._save_search(name, search)
|
|
|
|
|
|
|
|
def update_saved_search(self, item: SidebarItem) -> None:
|
|
|
|
if (search := self._get_current_search()) is None:
|
2021-03-04 17:22:03 +01:00
|
|
|
return
|
2021-03-23 11:20:46 +01:00
|
|
|
self._save_search(item.name, search, update=True)
|
2021-01-20 00:00:53 +01:00
|
|
|
|
2021-03-13 09:45:06 +01:00
|
|
|
# Notetypes and templates
|
|
|
|
####################################
|
|
|
|
|
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
|
|
|
|
2021-03-10 10:14:06 +01:00
|
|
|
def manage_template(self, item: SidebarItem) -> None:
|
|
|
|
note = Note(self.col, self.col.models.get(item._parent_item.id))
|
|
|
|
CardLayout(self.mw, note, ord=item.id, parent=self, fill_empty=True)
|
|
|
|
|
2021-02-25 21:24:11 +01:00
|
|
|
# Helpers
|
2021-03-13 09:45:06 +01:00
|
|
|
####################################
|
2021-02-25 21:24:11 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
2021-03-22 14:43:54 +01:00
|
|
|
def _selected_decks(self) -> List[DeckID]:
|
2021-02-26 19:52:34 +01:00
|
|
|
return [
|
2021-03-22 14:43:54 +01:00
|
|
|
DeckID(item.id)
|
2021-02-26 19:52:34 +01:00
|
|
|
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
|
|
|
|
]
|