feat: add category creation and renaming
This commit is contained in:
@@ -18,6 +18,7 @@ from PySide6.QtCore import (
|
||||
QObject,
|
||||
QSortFilterProxyModel,
|
||||
Qt,
|
||||
QTimer,
|
||||
Signal,
|
||||
)
|
||||
|
||||
@@ -400,7 +401,7 @@ class CategoryTreeModel(QAbstractItemModel):
|
||||
|
||||
item: CategoryTreeItem = index.internalPointer()
|
||||
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
|
||||
return item.name
|
||||
|
||||
if role == Qt.ItemDataRole.UserRole:
|
||||
@@ -438,6 +439,7 @@ class CategoryTreeModel(QAbstractItemModel):
|
||||
default_flags
|
||||
| Qt.ItemFlag.ItemIsDragEnabled
|
||||
| Qt.ItemFlag.ItemIsDropEnabled
|
||||
| Qt.ItemFlag.ItemIsEditable
|
||||
)
|
||||
|
||||
def supportedDropActions(self) -> Qt.DropAction: # noqa: N802
|
||||
@@ -642,6 +644,81 @@ class CategoryTreeModel(QAbstractItemModel):
|
||||
return None
|
||||
return index.internalPointer()
|
||||
|
||||
def setData( # noqa: N802
|
||||
self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole
|
||||
) -> bool:
|
||||
if role != Qt.ItemDataRole.EditRole:
|
||||
return False
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
item: CategoryTreeItem = index.internalPointer()
|
||||
if item.full_path == "Top Level":
|
||||
return False
|
||||
|
||||
new_name = str(value).strip() if value else ""
|
||||
if not new_name or new_name == item.name:
|
||||
return False
|
||||
|
||||
if not self._virtual_tree or not self._logic:
|
||||
return False
|
||||
|
||||
return self.rename_category(index, new_name)
|
||||
|
||||
def create_category(self, name: str, parent_path: str | None = None) -> QModelIndex:
|
||||
if not self._virtual_tree or not self._logic:
|
||||
return QModelIndex()
|
||||
|
||||
new_path = f"{parent_path}:{name}" if parent_path else name
|
||||
|
||||
self.backup_requested.emit(True) # noqa: FBT003
|
||||
|
||||
try:
|
||||
if not self._virtual_tree.create_category(new_path, self._logic):
|
||||
return QModelIndex()
|
||||
except CategoryError as e:
|
||||
self.error_occurred.emit("Category Creation Failed", str(e))
|
||||
return QModelIndex()
|
||||
|
||||
self._rebuild_from_virtual()
|
||||
return self.index_for_path(new_path)
|
||||
|
||||
def rename_category(self, index: QModelIndex, new_name: str) -> bool:
|
||||
if not index.isValid() or not self._virtual_tree or not self._logic:
|
||||
return False
|
||||
|
||||
item: CategoryTreeItem = index.internalPointer()
|
||||
if item.full_path == "Top Level":
|
||||
return False
|
||||
|
||||
node = self._virtual_tree.get_node(item.full_path)
|
||||
if node is None:
|
||||
return False
|
||||
|
||||
all_nodes = node.all_nodes_flat()
|
||||
old_path_to_node = [(n.full_path, n) for n in all_nodes]
|
||||
|
||||
if not self._virtual_tree.rename_category(node, new_name):
|
||||
return False
|
||||
|
||||
changed = {
|
||||
old_path: n.full_path
|
||||
for old_path, n in old_path_to_node
|
||||
if old_path != n.full_path
|
||||
}
|
||||
|
||||
self.backup_requested.emit(True) # noqa: FBT003
|
||||
|
||||
try:
|
||||
self._virtual_tree.sync_to_logic(self._logic, changed if changed else None)
|
||||
self._virtual_tree.update_plugin_counts(self._logic)
|
||||
except CategoryError as e:
|
||||
self.error_occurred.emit("Category Rename Failed", str(e))
|
||||
return False
|
||||
|
||||
QTimer.singleShot(0, self._rebuild_from_virtual)
|
||||
return True
|
||||
|
||||
|
||||
class ManufacturerListModel(QAbstractListModel):
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from AppKit import (
|
||||
NSColor, # type: ignore[attr-defined]
|
||||
NSFontWeightBold, # type: ignore[attr-defined]
|
||||
NSFontWeightRegular, # type: ignore[attr-defined]
|
||||
NSGraphicsContext, # type: ignore[attr-defined]
|
||||
NSImage, # type: ignore[attr-defined]
|
||||
@@ -20,7 +21,7 @@ from Quartz import (
|
||||
kCGImageAlphaPremultipliedLast, # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
_icon_cache: dict[tuple[str, int, tuple[float, ...] | None], QIcon] = {}
|
||||
_icon_cache: dict[tuple[str, int, tuple[float, ...] | None, bool], QIcon] = {}
|
||||
|
||||
SCALE_FACTOR = 2
|
||||
|
||||
@@ -28,10 +29,14 @@ DEFAULT_COLOR = (155.0 / 255.0, 153.0 / 255.0, 158.0 / 255.0, 1.0)
|
||||
|
||||
|
||||
def sf_symbol(
|
||||
name: str, size: int = 16, color: tuple[float, float, float, float] | None = None
|
||||
name: str,
|
||||
size: int = 16,
|
||||
color: tuple[float, float, float, float] | None = None,
|
||||
*,
|
||||
bold: bool = False,
|
||||
) -> QIcon:
|
||||
color_key = color if color else None
|
||||
cache_key = (name, size, color_key)
|
||||
cache_key = (name, size, color_key, bold)
|
||||
if cache_key in _icon_cache:
|
||||
return _icon_cache[cache_key]
|
||||
|
||||
@@ -39,8 +44,9 @@ def sf_symbol(
|
||||
if ns_image is None:
|
||||
return QIcon()
|
||||
|
||||
weight = NSFontWeightBold if bold else NSFontWeightRegular
|
||||
size_config = NSImageSymbolConfiguration.configurationWithPointSize_weight_scale_(
|
||||
float(size), NSFontWeightRegular, NSImageSymbolScaleMedium
|
||||
float(size), weight, NSImageSymbolScaleMedium
|
||||
)
|
||||
r, g, b, a = color if color else DEFAULT_COLOR
|
||||
icon_color = NSColor.colorWithSRGBRed_green_blue_alpha_(r, g, b, a)
|
||||
|
||||
@@ -472,3 +472,60 @@ class VirtualCategoryTree:
|
||||
if n.full_path != old_path:
|
||||
result[n.full_path.replace(old_path, node.full_path, 1)] = n.full_path
|
||||
return result
|
||||
|
||||
def create_category(self, path: str, logic: Logic) -> bool:
|
||||
if path in self._nodes:
|
||||
return False
|
||||
if path == "Top Level":
|
||||
return False
|
||||
|
||||
parts = path.split(":")
|
||||
parent_path = ":".join(parts[:-1]) if len(parts) > 1 else ""
|
||||
name = parts[-1]
|
||||
|
||||
if parent_path:
|
||||
parent_node = self.get_node(parent_path)
|
||||
if parent_node is None:
|
||||
return False
|
||||
else:
|
||||
parent_node = self._root
|
||||
|
||||
node = VirtualCategoryNode(
|
||||
name=name, full_path=path, parent=parent_node, plugin_count=0
|
||||
)
|
||||
parent_node.children.append(node)
|
||||
self._nodes[path] = node
|
||||
|
||||
with contextlib.suppress(CategoryExistsError):
|
||||
logic.introduce_category(path)
|
||||
|
||||
return True
|
||||
|
||||
def rename_category(self, node: VirtualCategoryNode, new_name: str) -> bool:
|
||||
if node.full_path == "Top Level":
|
||||
return False
|
||||
if not new_name or ":" in new_name:
|
||||
return False
|
||||
|
||||
old_path = node.full_path
|
||||
|
||||
if node.parent and node.parent.full_path:
|
||||
new_path = f"{node.parent.full_path}:{new_name}"
|
||||
else:
|
||||
new_path = new_name
|
||||
|
||||
if new_path in self._nodes and self._nodes[new_path] != node:
|
||||
return False
|
||||
|
||||
del self._nodes[old_path]
|
||||
node.name = new_name
|
||||
node.full_path = new_path
|
||||
self._nodes[new_path] = node
|
||||
|
||||
for desc in node.descendants_flat():
|
||||
old_desc_path = desc.full_path
|
||||
del self._nodes[old_desc_path]
|
||||
desc.full_path = desc.full_path.replace(old_path, new_path, 1)
|
||||
self._nodes[desc.full_path] = desc
|
||||
|
||||
return True
|
||||
|
||||
@@ -12,6 +12,7 @@ from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QListView,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
@@ -110,6 +111,19 @@ class _VimTreeView(QTreeView):
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||
index = self.indexAt(event.position().toPoint())
|
||||
if index.isValid():
|
||||
item: CategoryTreeItem = index.internalPointer()
|
||||
if item.children:
|
||||
if self.isExpanded(index):
|
||||
self.collapse(index)
|
||||
else:
|
||||
self.expand(index)
|
||||
event.accept()
|
||||
return
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802, C901, PLR0911, PLR0912
|
||||
vk = event.nativeVirtualKey()
|
||||
key = event.key()
|
||||
@@ -237,6 +251,18 @@ class _VimTreeView(QTreeView):
|
||||
""")
|
||||
glass.prepare_window_for_glass(menu)
|
||||
|
||||
rename_action = menu.addAction(sf_symbol("pencil", 14), "Rename")
|
||||
rename_action.triggered.connect(lambda: self._do_rename(index))
|
||||
|
||||
create_sub_action = menu.addAction(
|
||||
sf_symbol("folder.badge.plus", 14), "Create Subcategory"
|
||||
)
|
||||
create_sub_action.triggered.connect(
|
||||
lambda: self._do_create_subcategory(model, index)
|
||||
)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
move_up_action = menu.addAction(sf_symbol("arrow.up", 14), "Move Up")
|
||||
move_up_action.setShortcut("Alt+Shift+Up")
|
||||
move_up_action.triggered.connect(lambda: self._do_move_up(model, index))
|
||||
@@ -277,6 +303,19 @@ class _VimTreeView(QTreeView):
|
||||
model.extract_category(index)
|
||||
self._restore_selection_after_move(index)
|
||||
|
||||
def _do_rename(self, index: QModelIndex) -> None:
|
||||
self.edit(index)
|
||||
|
||||
def _do_create_subcategory(
|
||||
self, model: CategoryTreeModel, index: QModelIndex
|
||||
) -> None:
|
||||
item: CategoryTreeItem = index.internalPointer()
|
||||
new_index = model.create_category("Untitled", item.full_path)
|
||||
if new_index.isValid():
|
||||
self.expand(index)
|
||||
self.setCurrentIndex(new_index)
|
||||
self.edit(new_index)
|
||||
|
||||
|
||||
class _VimListView(QListView):
|
||||
enter_pressed = Signal()
|
||||
@@ -362,6 +401,19 @@ class _CategoryDelegate(QStyledItemDelegate):
|
||||
|
||||
super().paint(painter, option, index)
|
||||
|
||||
def setEditorData( # noqa: N802
|
||||
self, editor: QWidget, index: QModelIndex
|
||||
) -> None:
|
||||
from PySide6.QtWidgets import QLineEdit # noqa: PLC0415
|
||||
|
||||
if isinstance(editor, QLineEdit):
|
||||
text = index.data(Qt.ItemDataRole.EditRole)
|
||||
if text:
|
||||
editor.setText(str(text))
|
||||
editor.selectAll()
|
||||
else:
|
||||
super().setEditorData(editor, index)
|
||||
|
||||
|
||||
class StickyItem(QWidget):
|
||||
clicked = Signal(str)
|
||||
@@ -437,6 +489,43 @@ class _SectionHeader(QWidget):
|
||||
layout.addStretch()
|
||||
|
||||
|
||||
class _CategorySectionHeader(_SectionHeader):
|
||||
add_clicked = Signal()
|
||||
|
||||
def __init__(
|
||||
self, title: str, icon_name: str, parent: QWidget | None = None
|
||||
) -> None:
|
||||
super().__init__(title, icon_name, parent)
|
||||
|
||||
from PySide6.QtCore import QSize # noqa: PLC0415
|
||||
|
||||
self._add_button = QPushButton()
|
||||
self._add_button.setFixedSize(16, 16)
|
||||
self._add_button.setIconSize(QSize(10, 10))
|
||||
self._add_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
icon = sf_symbol("plus", 10, (0.25, 0.25, 0.27, 1.0), bold=True)
|
||||
if not icon.isNull():
|
||||
self._add_button.setIcon(icon)
|
||||
self._add_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #9B999E;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #ADABAF;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #8A888D;
|
||||
}
|
||||
""")
|
||||
self._add_button.clicked.connect(self.add_clicked)
|
||||
|
||||
layout = self.layout()
|
||||
if layout is not None:
|
||||
layout.addWidget(self._add_button)
|
||||
|
||||
|
||||
class _DraggableHeader(_SectionHeader):
|
||||
dragged = Signal(int)
|
||||
|
||||
@@ -525,7 +614,8 @@ class Sidebar(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
header = _SectionHeader("Category", "folder")
|
||||
header = _CategorySectionHeader("Category", "folder")
|
||||
header.add_clicked.connect(self._on_add_category_clicked)
|
||||
layout.addWidget(header)
|
||||
|
||||
tree_container = QWidget()
|
||||
@@ -649,6 +739,16 @@ class Sidebar(QWidget):
|
||||
self._manufacturer_list.clearSelection()
|
||||
self.category_selected.emit(full_path)
|
||||
|
||||
def _on_add_category_clicked(self) -> None:
|
||||
index = self._category_model.create_category("Untitled")
|
||||
if index.isValid():
|
||||
parent = index.parent()
|
||||
while parent.isValid():
|
||||
self._category_tree.expand(parent)
|
||||
parent = parent.parent()
|
||||
self._category_tree.setCurrentIndex(index)
|
||||
self._category_tree.edit(index)
|
||||
|
||||
def _on_manufacturer_clicked(self, index: QModelIndex) -> None:
|
||||
manufacturer = index.data(Qt.ItemDataRole.DisplayRole)
|
||||
if manufacturer:
|
||||
|
||||
Reference in New Issue
Block a user