Compare commits

...

2 Commits

4 changed files with 573 additions and 13 deletions

View File

@@ -136,10 +136,39 @@ class PluginTableModel(QAbstractTableModel):
def flags(self, index: QModelIndex) -> Qt.ItemFlag: def flags(self, index: QModelIndex) -> Qt.ItemFlag:
base_flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable base_flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
if index.isValid():
base_flags |= Qt.ItemFlag.ItemIsDragEnabled
if index.column() in (COL_CUSTOM_NAME, COL_SHORT_NAME): if index.column() in (COL_CUSTOM_NAME, COL_SHORT_NAME):
return base_flags | Qt.ItemFlag.ItemIsEditable return base_flags | Qt.ItemFlag.ItemIsEditable
return base_flags return base_flags
def mimeTypes(self) -> list[str]: # noqa: N802
return [PLUGIN_MIME_TYPE]
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData: # noqa: N802
mime_data = QMimeData()
if not indexes:
return mime_data
rows = set()
for index in indexes:
if index.isValid():
rows.add(index.row())
plugin_ids = []
for row in sorted(rows):
plugin = self.get_plugin(row)
if plugin:
plugin_ids.append(
f"{plugin.name}|{plugin.manufacturer}|{plugin.type_code}"
)
if plugin_ids:
data = "\n".join(plugin_ids)
mime_data.setData(PLUGIN_MIME_TYPE, QByteArray(data.encode("utf-8")))
return mime_data
def setData( # noqa: N802 def setData( # noqa: N802
self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole
) -> bool: ) -> bool:
@@ -298,12 +327,14 @@ class CategoryTreeItem:
CATEGORY_MIME_TYPE = "application/x-illogical-category" CATEGORY_MIME_TYPE = "application/x-illogical-category"
PLUGIN_MIME_TYPE = "application/x-illogical-plugin"
class CategoryTreeModel(QAbstractItemModel): class CategoryTreeModel(QAbstractItemModel):
category_changed = Signal() category_changed = Signal()
error_occurred = Signal(str, str) error_occurred = Signal(str, str)
backup_requested = Signal(bool) backup_requested = Signal(bool)
plugins_dropped = Signal(list, str, bool)
def __init__(self, parent: QObject | None = None) -> None: def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent) super().__init__(parent)
@@ -443,10 +474,10 @@ class CategoryTreeModel(QAbstractItemModel):
) )
def supportedDropActions(self) -> Qt.DropAction: # noqa: N802 def supportedDropActions(self) -> Qt.DropAction: # noqa: N802
return Qt.DropAction.MoveAction return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
def mimeTypes(self) -> list[str]: # noqa: N802 def mimeTypes(self) -> list[str]: # noqa: N802
return [CATEGORY_MIME_TYPE] return [CATEGORY_MIME_TYPE, PLUGIN_MIME_TYPE]
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData: # noqa: N802 def mimeData(self, indexes: list[QModelIndex]) -> QMimeData: # noqa: N802
mime_data = QMimeData() mime_data = QMimeData()
@@ -473,6 +504,9 @@ class CategoryTreeModel(QAbstractItemModel):
column: int, # noqa: ARG002 column: int, # noqa: ARG002
parent: QModelIndex, parent: QModelIndex,
) -> bool: ) -> bool:
if data.hasFormat(PLUGIN_MIME_TYPE):
return self._handle_plugin_drop(data, parent)
if action != Qt.DropAction.MoveAction: if action != Qt.DropAction.MoveAction:
return False return False
if not data.hasFormat(CATEGORY_MIME_TYPE): if not data.hasFormat(CATEGORY_MIME_TYPE):
@@ -538,6 +572,27 @@ class CategoryTreeModel(QAbstractItemModel):
self._rebuild_from_virtual() self._rebuild_from_virtual()
return True return True
def _handle_plugin_drop(self, data: QMimeData, parent: QModelIndex) -> bool:
from PySide6.QtWidgets import QApplication # noqa: PLC0415
if not parent.isValid():
return False
item: CategoryTreeItem = parent.internalPointer()
target_category = "" if item.full_path == "Top Level" else item.full_path
modifiers = QApplication.keyboardModifiers()
is_add = bool(modifiers & Qt.KeyboardModifier.ShiftModifier)
is_move = not is_add
raw_data = data.data(PLUGIN_MIME_TYPE).data()
plugin_ids = bytes(raw_data).decode("utf-8").split("\n") if raw_data else []
if plugin_ids:
self.plugins_dropped.emit(plugin_ids, target_category, is_move)
return True
def move_category_up(self, index: QModelIndex) -> bool: def move_category_up(self, index: QModelIndex) -> bool:
if not index.isValid() or self._virtual_tree is None or self._logic is None: if not index.isValid() or self._virtual_tree is None or self._logic is None:
return False return False
@@ -644,6 +699,10 @@ class CategoryTreeModel(QAbstractItemModel):
return None return None
return index.internalPointer() return index.internalPointer()
@property
def root_item(self) -> CategoryTreeItem:
return self._root
def setData( # noqa: N802 def setData( # noqa: N802
self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole
) -> bool: ) -> bool:

View File

@@ -20,7 +20,7 @@ from illogical.ui.restore_backup_window import RestoreBackupWindow
from illogical.ui.sidebar import Sidebar from illogical.ui.sidebar import Sidebar
if TYPE_CHECKING: if TYPE_CHECKING:
from logic_plugin_manager import AudioComponent, Logic, SearchResult from logic_plugin_manager import AudioComponent, Category, Logic, SearchResult
from PySide6.QtGui import QCloseEvent, QKeyEvent, QShowEvent from PySide6.QtGui import QCloseEvent, QKeyEvent, QShowEvent
from illogical.modules.backup_models import ( from illogical.modules.backup_models import (
@@ -138,6 +138,12 @@ class MainWindow(QMainWindow):
self._backup_service.set_logic(logic) self._backup_service.set_logic(logic)
self._sidebar.populate(logic) self._sidebar.populate(logic)
self._plugin_table.set_plugins(logic) self._plugin_table.set_plugins(logic)
self._plugin_table.set_category_tree(self._sidebar.category_model)
self._sidebar.category_model.plugins_dropped.connect(self._on_plugins_dropped)
self._plugin_table.category_assignment_requested.connect(
self._on_category_assignment
)
self._plugin_table.category_removal_requested.connect(self._on_category_removal)
self._loading_overlay.hide() self._loading_overlay.hide()
self._plugin_table.focus_table() self._plugin_table.focus_table()
@@ -201,6 +207,131 @@ class MainWindow(QMainWindow):
BackupTrigger.AUTO, "Before category modification" BackupTrigger.AUTO, "Before category modification"
) )
def _on_plugins_dropped(
self,
plugin_ids: list[str],
category_path: str,
is_move: bool, # noqa: FBT001
) -> None:
if not self._logic:
return
plugins = []
for plugin_id in plugin_ids:
parts = plugin_id.split("|")
if len(parts) == 3: # noqa: PLR2004
name, manufacturer, type_code = parts
for plugin in self._logic.plugins.all():
if (
plugin.name == name
and plugin.manufacturer == manufacturer
and plugin.type_code == type_code
):
plugins.append(plugin)
break
if plugins:
self._assign_plugins_to_category(plugins, category_path, is_move)
def _on_category_assignment(
self,
plugins: list[AudioComponent],
category_path: str,
is_move: bool, # noqa: FBT001
) -> None:
self._assign_plugins_to_category(plugins, category_path, is_move)
def _on_category_removal(self, plugins: list[AudioComponent]) -> None:
if not self._logic or not self._current_category:
return
try:
if backup_manager.should_create_auto_backup():
backup_manager.create_backup(
BackupTrigger.AUTO,
f"Before removing {len(plugins)} plugin(s) from category",
)
category_path = self._current_category
if category_path == "Top Level":
category_path = ""
category = self._logic.categories.get(category_path)
if category:
for plugin in plugins:
plugin.remove_from_category(category)
self._logic.sync_category_plugin_amount(category)
self._sidebar.populate(self._logic)
self._plugin_table.filter_by_category(self._current_category)
except Exception as e: # noqa: BLE001
QMessageBox.warning(
self, "Category Removal Failed", f"Failed to remove plugins: {e}"
)
def _assign_plugins_to_category(
self,
plugins: list[AudioComponent],
category_path: str,
is_move: bool, # noqa: FBT001
) -> None:
if not self._logic:
return
try:
if backup_manager.should_create_auto_backup():
action = "Moving" if is_move else "Assigning"
backup_manager.create_backup(
BackupTrigger.AUTO,
f"Before {action.lower()} {len(plugins)} plugin(s) to category",
)
if category_path:
category = self._logic.categories.get(category_path)
if not category:
category = self._logic.introduce_category(category_path)
else:
category = self._logic.categories.get("")
if not category:
category = self._logic.introduce_category("")
for plugin in plugins:
self._add_plugin_to_category(plugin, category, is_move)
self._logic.sync_category_plugin_amount(category)
self._sidebar.populate(self._logic)
if self._current_manufacturer:
self._plugin_table.filter_by_manufacturer(self._current_manufacturer)
else:
self._plugin_table.filter_by_category(self._current_category)
except Exception as e: # noqa: BLE001
QMessageBox.warning(
self, "Category Assignment Failed", f"Failed to assign plugins: {e}"
)
def _add_plugin_to_category(
self,
plugin: AudioComponent,
category: Category,
is_move: bool, # noqa: FBT001
) -> None:
current_tags = getattr(plugin.tagset, "tags", None)
if current_tags is None:
plugin.tagset.load()
current_tags = plugin.tagset.tags
if is_move:
new_tags = {category.name: "user"}
else:
new_tags = dict(current_tags)
new_tags[category.name] = "user"
plugin.tagset.set_tags(new_tags)
plugin.load()
def _on_search_results(self, results: list[SearchResult]) -> None: def _on_search_results(self, results: list[SearchResult]) -> None:
plugins = [r.plugin for r in results] plugins = [r.plugin for r in results]
self._plugin_table.filter_by_search_results( self._plugin_table.filter_by_search_results(

View File

@@ -2,11 +2,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import QItemSelection, QPoint, Qt, QTimer, Signal
from PySide6.QtGui import QCursor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QFrame, QFrame,
QHeaderView, QHeaderView,
QMenu,
QTableView, QTableView,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@@ -19,22 +21,70 @@ from illogical.modules.models import (
COL_SHORT_NAME, COL_SHORT_NAME,
COL_TYPE, COL_TYPE,
COL_VERSION, COL_VERSION,
CategoryTreeItem,
CategoryTreeModel,
PluginTableModel, PluginTableModel,
) )
from illogical.ui.search_bar import SearchBar from illogical.ui.search_bar import SearchBar
if TYPE_CHECKING: if TYPE_CHECKING:
from logic_plugin_manager import AudioComponent, Logic from logic_plugin_manager import AudioComponent, Logic
from PySide6.QtCore import QModelIndex from PySide6.QtCore import QEvent, QModelIndex
from PySide6.QtGui import QKeyEvent, QResizeEvent from PySide6.QtGui import QKeyEvent, QMouseEvent, QResizeEvent
KVK_J = 0x26 KVK_J = 0x26
KVK_K = 0x28 KVK_K = 0x28
KVK_V = 0x09
class _VimTableView(QTableView): class _VimTableView(QTableView):
enter_pressed = Signal() enter_pressed = Signal()
context_menu_requested = Signal(QPoint)
visual_mode_changed = Signal(bool)
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._anchor_row: int | None = None
self._visual_line_mode: bool = False
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
self.setDefaultDropAction(Qt.DropAction.CopyAction)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._on_context_menu)
def _on_context_menu(self, pos: QPoint) -> None:
if self.selectionModel().hasSelection():
self.context_menu_requested.emit(pos)
def event(self, e: QEvent) -> bool:
from PySide6.QtCore import QEvent as QEventType # noqa: PLC0415
from PySide6.QtGui import QKeyEvent as QKeyEventType # noqa: PLC0415
if (
e.type() == QEventType.Type.ShortcutOverride
and isinstance(e, QKeyEventType)
and e.key() == Qt.Key.Key_A
and e.modifiers() == Qt.KeyboardModifier.ControlModifier
):
e.accept()
return True
return super().event(e)
def _select_all_rows(self) -> None:
row_count = self.model().rowCount()
if row_count > 0:
top_left = self.model().index(0, 0)
bottom_right = self.model().index(
row_count - 1, self.model().columnCount() - 1
)
selection = QItemSelection(top_left, bottom_right)
self.selectionModel().select(
selection,
self.selectionModel().SelectionFlag.ClearAndSelect
| self.selectionModel().SelectionFlag.Rows,
)
self._anchor_row = 0
def _select_row(self, row: int) -> None: def _select_row(self, row: int) -> None:
index = self.model().index(row, 0) index = self.model().index(row, 0)
@@ -43,21 +93,143 @@ class _VimTableView(QTableView):
self.selectionModel().SelectionFlag.ClearAndSelect self.selectionModel().SelectionFlag.ClearAndSelect
| self.selectionModel().SelectionFlag.Rows, | self.selectionModel().SelectionFlag.Rows,
) )
self._anchor_row = row
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802 def _extend_selection_to(self, row: int) -> None:
if self._anchor_row is None:
self._anchor_row = self.currentIndex().row()
start = min(self._anchor_row, row)
end = max(self._anchor_row, row)
self.selectionModel().clear()
for r in range(start, end + 1):
idx = self.model().index(r, 0)
self.selectionModel().select(
idx,
self.selectionModel().SelectionFlag.Select
| self.selectionModel().SelectionFlag.Rows,
)
index = self.model().index(row, 0)
self.selectionModel().setCurrentIndex(
index, self.selectionModel().SelectionFlag.NoUpdate
)
def enter_visual_line_mode(self) -> None:
if self._visual_line_mode:
return
self._visual_line_mode = True
current = self.currentIndex()
if current.isValid():
self._anchor_row = current.row()
self._select_row(current.row())
self.visual_mode_changed.emit(True) # noqa: FBT003
def exit_visual_line_mode(self) -> None:
if not self._visual_line_mode:
return
self._visual_line_mode = False
self.visual_mode_changed.emit(False) # noqa: FBT003
def is_visual_line_mode(self) -> bool:
return self._visual_line_mode
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
if self._visual_line_mode:
self.exit_visual_line_mode()
index = self.indexAt(event.position().toPoint())
if not index.isValid():
super().mousePressEvent(event)
return
mods = event.modifiers()
has_cmd = bool(mods & Qt.KeyboardModifier.ControlModifier)
has_shift = bool(mods & Qt.KeyboardModifier.ShiftModifier)
if has_cmd and not has_shift:
self.selectionModel().setCurrentIndex(
index,
self.selectionModel().SelectionFlag.Toggle
| self.selectionModel().SelectionFlag.Rows,
)
if self._anchor_row is None:
self._anchor_row = index.row()
event.accept()
return
if has_shift and self._anchor_row is not None:
self.selectionModel().clear()
start = min(self._anchor_row, index.row())
end = max(self._anchor_row, index.row())
for r in range(start, end + 1):
idx = self.model().index(r, 0)
self.selectionModel().select(
idx,
self.selectionModel().SelectionFlag.Select
| self.selectionModel().SelectionFlag.Rows,
)
self.selectionModel().setCurrentIndex(
index, self.selectionModel().SelectionFlag.NoUpdate
)
event.accept()
return
self._anchor_row = index.row()
super().mousePressEvent(event)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802, C901, PLR0911, PLR0912
vk = event.nativeVirtualKey() vk = event.nativeVirtualKey()
key = event.key() key = event.key()
mods = event.modifiers()
has_shift = bool(mods & Qt.KeyboardModifier.ShiftModifier)
has_alt = bool(mods & Qt.KeyboardModifier.AltModifier)
has_ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier)
if key == Qt.Key.Key_Escape and self._visual_line_mode:
self.exit_visual_line_mode()
current = self.currentIndex()
if current.isValid():
self._select_row(current.row())
event.accept()
return
if has_shift and vk == KVK_V:
self.enter_visual_line_mode()
event.accept()
return
if has_ctrl and key == Qt.Key.Key_A:
self._select_all_rows()
event.accept()
return
if has_alt and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
self.context_menu_requested.emit(QPoint(0, 0))
event.accept()
return
extend_selection = has_shift or self._visual_line_mode
if vk == KVK_J or key == Qt.Key.Key_Down: if vk == KVK_J or key == Qt.Key.Key_Down:
current = self.currentIndex() current = self.currentIndex()
if current.row() < self.model().rowCount() - 1: if current.row() < self.model().rowCount() - 1:
self._select_row(current.row() + 1) new_row = current.row() + 1
if extend_selection:
self._extend_selection_to(new_row)
else:
self._select_row(new_row)
event.accept() event.accept()
return return
if vk == KVK_K or key == Qt.Key.Key_Up: if vk == KVK_K or key == Qt.Key.Key_Up:
current = self.currentIndex() current = self.currentIndex()
if current.row() > 0: if current.row() > 0:
self._select_row(current.row() - 1) new_row = current.row() - 1
if extend_selection:
self._extend_selection_to(new_row)
else:
self._select_row(new_row)
event.accept() event.accept()
return return
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
@@ -66,14 +238,29 @@ class _VimTableView(QTableView):
return return
super().keyPressEvent(event) super().keyPressEvent(event)
def get_selected_plugins(self) -> list[AudioComponent]:
model = self.model()
if not isinstance(model, PluginTableModel):
return []
plugins = []
for index in self.selectionModel().selectedRows():
plugin = model.get_plugin(index.row())
if plugin:
plugins.append(plugin)
return plugins
class PluginTableView(QWidget): class PluginTableView(QWidget):
search_changed = Signal(str) search_changed = Signal(str)
plugin_selected = Signal(object) plugin_selected = Signal(object)
edit_requested = Signal(object, int, str) edit_requested = Signal(object, int, str)
category_assignment_requested = Signal(list, str, bool)
category_removal_requested = Signal(list)
visual_mode_changed = Signal(bool)
def __init__(self, parent: QWidget | None = None) -> None: def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent) super().__init__(parent)
self._category_tree: CategoryTreeModel | None = None
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setContentsMargins(16, 0, 16, 16) layout.setContentsMargins(16, 0, 16, 16)
@@ -90,7 +277,7 @@ class PluginTableView(QWidget):
self._table.setModel(self._model) self._table.setModel(self._model)
self._table.setAlternatingRowColors(True) self._table.setAlternatingRowColors(True)
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self._table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self._table.setShowGrid(False) self._table.setShowGrid(False)
self._table.verticalHeader().setVisible(False) self._table.verticalHeader().setVisible(False)
self._table.horizontalHeader().setStretchLastSection(True) self._table.horizontalHeader().setStretchLastSection(True)
@@ -138,6 +325,8 @@ class PluginTableView(QWidget):
self._table.selectionModel().currentChanged.connect(self._on_current_changed) self._table.selectionModel().currentChanged.connect(self._on_current_changed)
self._table.enter_pressed.connect(self._on_enter_pressed) self._table.enter_pressed.connect(self._on_enter_pressed)
self._table.context_menu_requested.connect(self._on_context_menu_requested)
self._table.visual_mode_changed.connect(self.visual_mode_changed)
def _on_current_changed(self, current: QModelIndex, _previous: QModelIndex) -> None: def _on_current_changed(self, current: QModelIndex, _previous: QModelIndex) -> None:
if current.isValid(): if current.isValid():
@@ -152,6 +341,137 @@ class PluginTableView(QWidget):
if plugin: if plugin:
self.plugin_selected.emit(plugin) self.plugin_selected.emit(plugin)
def set_category_tree(self, tree: CategoryTreeModel) -> None:
self._category_tree = tree
def _on_context_menu_requested(self, pos: QPoint) -> None: # noqa: ARG002
plugins = self._table.get_selected_plugins()
if not plugins:
return
self._show_context_menu(plugins)
def _show_context_menu(self, plugins: list[AudioComponent]) -> None:
import pyqt_liquidglass as glass # noqa: PLC0415
opts = glass.GlassOptions(corner_radius=10.0)
def apply_glass_on_show(m: QMenu) -> None:
glass.prepare_window_for_glass(m)
glass.apply_glass_to_window(m, opts)
menu = QMenu(self)
menu.setStyleSheet(self._get_glass_menu_stylesheet())
glass.prepare_window_for_glass(menu)
assign_menu = menu.addMenu("Assign to")
assign_menu.setStyleSheet(self._get_glass_menu_stylesheet())
assign_menu.aboutToShow.connect(lambda: apply_glass_on_show(assign_menu))
self._build_category_submenu(assign_menu, plugins, is_move=False)
has_categories = any(p.categories for p in plugins)
if has_categories:
move_menu = menu.addMenu("Move to")
move_menu.setStyleSheet(self._get_glass_menu_stylesheet())
move_menu.aboutToShow.connect(lambda: apply_glass_on_show(move_menu))
self._build_category_submenu(move_menu, plugins, is_move=True)
menu.addSeparator()
remove_action = menu.addAction("Remove from current category")
remove_action.triggered.connect(
lambda: self._on_remove_from_category(plugins)
)
menu.popup(QCursor.pos())
QTimer.singleShot(0, lambda: glass.apply_glass_to_window(menu, opts))
def _build_category_submenu(
self, menu: QMenu, plugins: list[AudioComponent], *, is_move: bool
) -> None:
import pyqt_liquidglass as glass # noqa: PLC0415
if self._category_tree is None:
return
def apply_glass_on_show(submenu: QMenu) -> None:
glass.prepare_window_for_glass(submenu)
opts = glass.GlassOptions(corner_radius=8.0)
glass.apply_glass_to_window(submenu, opts)
def build_from_item(parent_menu: QMenu, item: CategoryTreeItem) -> None:
for child in item.children:
if child.full_path == "Top Level":
action = parent_menu.addAction("Top Level")
action.triggered.connect(
lambda _=None, p=plugins, m=is_move: self._on_category_action(
p, "", m
)
)
elif child.children:
submenu = parent_menu.addMenu(child.name)
submenu.setStyleSheet(self._get_glass_menu_stylesheet())
submenu.aboutToShow.connect(
lambda s=submenu: apply_glass_on_show(s)
)
self_action = submenu.addAction(f"{child.name}")
self_action.triggered.connect(
lambda _=None, p=plugins, c=child.full_path, m=is_move: (
self._on_category_action(p, c, m)
)
)
submenu.addSeparator()
build_from_item(submenu, child)
else:
action = parent_menu.addAction(child.name)
action.triggered.connect(
lambda _=None, p=plugins, c=child.full_path, m=is_move: (
self._on_category_action(p, c, m)
)
)
build_from_item(menu, self._category_tree.root_item)
def _on_category_action(
self,
plugins: list[AudioComponent],
category_path: str,
is_move: bool, # noqa: FBT001
) -> None:
self.category_assignment_requested.emit(plugins, category_path, is_move)
def _on_remove_from_category(self, plugins: list[AudioComponent]) -> None:
self.category_removal_requested.emit(plugins)
def _get_glass_menu_stylesheet(self) -> str:
return """
QMenu {
background: transparent;
border: none;
border-radius: 10px;
padding: 4px 2px;
}
QMenu::item {
padding: 4px 12px 4px 6px;
margin: 0px 2px;
border-radius: 6px;
color: rgba(255, 255, 255, 0.9);
}
QMenu::item:selected {
background-color: rgba(255, 255, 255, 0.15);
}
QMenu::separator {
height: 1px;
background: rgba(255, 255, 255, 0.12);
margin: 4px 10px;
}
QMenu::right-arrow {
width: 8px;
height: 8px;
right: 9px;
top: -1px;
}
"""
def set_plugins(self, logic: Logic) -> None: def set_plugins(self, logic: Logic) -> None:
self._model.set_plugins(logic) self._model.set_plugins(logic)
self._resize_columns() self._resize_columns()
@@ -195,6 +515,9 @@ class PluginTableView(QWidget):
if plugin: if plugin:
self.plugin_selected.emit(plugin) self.plugin_selected.emit(plugin)
def is_visual_line_mode(self) -> bool:
return self._table.is_visual_line_mode()
def _on_search_escape(self) -> None: def _on_search_escape(self) -> None:
self._table.setFocus() self._table.setFocus()

View File

@@ -4,7 +4,14 @@ from typing import TYPE_CHECKING
from AppKit import NSColor # type: ignore[attr-defined] from AppKit import NSColor # type: ignore[attr-defined]
from PySide6.QtCore import QModelIndex, QPoint, QRect, Qt, QTimer, Signal from PySide6.QtCore import QModelIndex, QPoint, QRect, Qt, QTimer, Signal
from PySide6.QtGui import QCursor, QFont from PySide6.QtGui import (
QCursor,
QDragEnterEvent,
QDragLeaveEvent,
QDragMoveEvent,
QDropEvent,
QFont,
)
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QFrame, QFrame,
@@ -23,6 +30,7 @@ from PySide6.QtWidgets import (
) )
from illogical.modules.models import ( from illogical.modules.models import (
PLUGIN_MIME_TYPE,
CategoryTreeItem, CategoryTreeItem,
CategoryTreeModel, CategoryTreeModel,
ManufacturerFilterProxy, ManufacturerFilterProxy,
@@ -50,12 +58,13 @@ class _VimTreeView(QTreeView):
self.setDragEnabled(True) self.setDragEnabled(True)
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
self.setDefaultDropAction(Qt.DropAction.MoveAction) self.setDefaultDropAction(Qt.DropAction.MoveAction)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._on_context_menu_requested) self.customContextMenuRequested.connect(self._on_context_menu_requested)
self._expanded_paths: set[str] = set() self._expanded_paths: set[str] = set()
self.context_menu_path: str | None = None self.context_menu_path: str | None = None
self.drop_target_path: str | None = None
def setModel(self, model: CategoryTreeModel | None) -> None: # noqa: N802 def setModel(self, model: CategoryTreeModel | None) -> None: # noqa: N802
old_model = self.model() old_model = self.model()
@@ -124,6 +133,36 @@ class _VimTreeView(QTreeView):
return return
super().mouseDoubleClickEvent(event) super().mouseDoubleClickEvent(event)
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
if event.mimeData().hasFormat(PLUGIN_MIME_TYPE):
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
def dragMoveEvent(self, event: QDragMoveEvent) -> None: # noqa: N802
index = self.indexAt(event.position().toPoint())
if event.mimeData().hasFormat(PLUGIN_MIME_TYPE):
if index.isValid():
item: CategoryTreeItem = index.internalPointer()
self.drop_target_path = item.full_path
else:
self.drop_target_path = None
self.viewport().update()
event.acceptProposedAction()
else:
self.drop_target_path = None
super().dragMoveEvent(event)
def dragLeaveEvent(self, event: QDragLeaveEvent) -> None: # noqa: N802
self.drop_target_path = None
self.viewport().update()
super().dragLeaveEvent(event)
def dropEvent(self, event: QDropEvent) -> None: # noqa: N802
self.drop_target_path = None
self.viewport().update()
super().dropEvent(event)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802, C901, PLR0911, PLR0912 def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802, C901, PLR0911, PLR0912
vk = event.nativeVirtualKey() vk = event.nativeVirtualKey()
key = event.key() key = event.key()
@@ -365,7 +404,11 @@ class _CategoryDelegate(QStyledItemDelegate):
isinstance(tree_view, _VimTreeView) isinstance(tree_view, _VimTreeView)
and tree_view.context_menu_path == full_path and tree_view.context_menu_path == full_path
) )
if is_context_target: is_drop_target = (
isinstance(tree_view, _VimTreeView)
and tree_view.drop_target_path == full_path
)
if is_context_target or is_drop_target:
painter.save() painter.save()
path = QPainterPath() path = QPainterPath()
path.addRoundedRect(option.rect.toRectF(), 4, 4) path.addRoundedRect(option.rect.toRectF(), 4, 4)
@@ -695,6 +738,10 @@ class Sidebar(QWidget):
self._category_model.build_from_plugins(logic) self._category_model.build_from_plugins(logic)
self._manufacturer_model.build_from_plugins(logic) self._manufacturer_model.build_from_plugins(logic)
@property
def category_model(self) -> CategoryTreeModel:
return self._category_model
def _clear_selections(self) -> None: def _clear_selections(self) -> None:
self._category_tree.clearSelection() self._category_tree.clearSelection()
self._manufacturer_list.clearSelection() self._manufacturer_list.clearSelection()