Separate out the model for the instrument list.
And populate it as instruments are coming in to not block the main thread.startup
parent
a6e06da89e
commit
947af52e87
|
@ -82,16 +82,17 @@ class InstrumentDBClient(object):
|
|||
async def start_scan(self) -> None:
|
||||
await self.__stub.call('START_SCAN')
|
||||
|
||||
def __handle_mutation(
|
||||
async def __handle_mutation(
|
||||
self,
|
||||
request: instrument_db_pb2.Mutations,
|
||||
response: empty_message_pb2.EmptyMessage,
|
||||
) -> None:
|
||||
for mutation in request.mutations:
|
||||
logger.info("Mutation received: %s", mutation)
|
||||
for idx, mutation in enumerate(request.mutations):
|
||||
if mutation.WhichOneof('type') == 'add_instrument':
|
||||
self.__instruments[mutation.add_instrument.uri] = mutation.add_instrument
|
||||
else:
|
||||
raise ValueError(mutation)
|
||||
|
||||
self.mutation_handlers.call(mutation)
|
||||
if idx % 10 == 0:
|
||||
await asyncio.sleep(0, loop=self.event_loop)
|
||||
|
|
|
@ -29,7 +29,8 @@ add_python_package(
|
|||
flowlayout.py
|
||||
gain_slider.py
|
||||
instrument_library.py
|
||||
instrument_library_test.py
|
||||
instrument_list.py
|
||||
instrument_list_test.py
|
||||
load_history.py
|
||||
misc.py
|
||||
mute_button.py
|
||||
|
|
|
@ -51,6 +51,7 @@ from . import project_registry
|
|||
from . import pipeline_perf_monitor
|
||||
from . import stat_monitor
|
||||
from . import settings_dialog
|
||||
from . import instrument_list
|
||||
from . import instrument_library
|
||||
from . import ui_base
|
||||
|
||||
|
@ -137,6 +138,7 @@ class EditorApp(ui_base.AbstractEditorApp):
|
|||
self.__pipeline_perf_monitor = None # type: pipeline_perf_monitor.PipelinePerfMonitor
|
||||
self.__stat_monitor = None # type: stat_monitor.StatMonitor
|
||||
self.default_style = None # type: str
|
||||
self.instrument_list = None # type: instrument_list.InstrumentList
|
||||
self.devices = None # type: device_list.DeviceList
|
||||
self.setup_complete = None # type: asyncio.Event
|
||||
self.__settings_dialog = None # type: settings_dialog.SettingsDialog
|
||||
|
@ -235,7 +237,7 @@ class EditorApp(ui_base.AbstractEditorApp):
|
|||
|
||||
progress = win.createSetupProgress()
|
||||
try:
|
||||
progress.setNumSteps(4)
|
||||
progress.setNumSteps(5)
|
||||
|
||||
logger.info("Creating StatMonitor.")
|
||||
self.__stat_monitor = stat_monitor.StatMonitor(context=self.context)
|
||||
|
@ -277,7 +279,18 @@ class EditorApp(ui_base.AbstractEditorApp):
|
|||
self.show_pipeline_perf_monitor_action.setChecked)
|
||||
|
||||
with progress.step("Scanning instruments..."):
|
||||
await self.createInstrumentDB()
|
||||
create_instrument_db_response = editor_main_pb2.CreateProcessResponse()
|
||||
await self.process.manager.call(
|
||||
'CREATE_INSTRUMENT_DB_PROCESS', None, create_instrument_db_response)
|
||||
instrument_db_address = create_instrument_db_response.address
|
||||
|
||||
self.instrument_db = instrument_db.InstrumentDBClient(
|
||||
self.process.event_loop, self.process.server)
|
||||
self.instrument_list = instrument_list.InstrumentList(context=self.context)
|
||||
self.instrument_list.setup()
|
||||
await self.instrument_db.setup()
|
||||
await self.instrument_db.connect(instrument_db_address)
|
||||
|
||||
self.__instrument_library_dialog = instrument_library.InstrumentLibraryDialog(
|
||||
context=self.context)
|
||||
await self.__instrument_library_dialog.setup()
|
||||
|
@ -356,6 +369,10 @@ class EditorApp(ui_base.AbstractEditorApp):
|
|||
await self.urid_mapper.cleanup(self.process.event_loop)
|
||||
self.urid_mapper = None
|
||||
|
||||
if self.instrument_list is not None:
|
||||
self.instrument_list.cleanup()
|
||||
self.instrument_list = None
|
||||
|
||||
if self.instrument_db is not None:
|
||||
await self.instrument_db.disconnect()
|
||||
await self.instrument_db.cleanup()
|
||||
|
@ -418,17 +435,6 @@ class EditorApp(ui_base.AbstractEditorApp):
|
|||
await self.node_db.setup()
|
||||
await self.node_db.connect(node_db_address)
|
||||
|
||||
async def createInstrumentDB(self) -> None:
|
||||
create_instrument_db_response = editor_main_pb2.CreateProcessResponse()
|
||||
await self.process.manager.call(
|
||||
'CREATE_INSTRUMENT_DB_PROCESS', None, create_instrument_db_response)
|
||||
instrument_db_address = create_instrument_db_response.address
|
||||
|
||||
self.instrument_db = instrument_db.InstrumentDBClient(
|
||||
self.process.event_loop, self.process.server)
|
||||
await self.instrument_db.setup()
|
||||
await self.instrument_db.connect(instrument_db_address)
|
||||
|
||||
async def createURIDMapper(self) -> None:
|
||||
create_urid_mapper_response = editor_main_pb2.CreateProcessResponse()
|
||||
await self.process.manager.call(
|
||||
|
|
|
@ -21,12 +21,10 @@
|
|||
# @end:license
|
||||
|
||||
import asyncio
|
||||
import bisect
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
import uuid
|
||||
from typing import cast, Any, Optional, Iterator, List, Tuple
|
||||
from typing import cast, Any, Optional, List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
|
@ -34,7 +32,6 @@ from PyQt5 import QtGui
|
|||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import instrument_db
|
||||
from noisicaa import core
|
||||
from noisicaa import audioproc
|
||||
from noisicaa import value_types
|
||||
from noisicaa.builtin_nodes.instrument import processor_messages as instrument
|
||||
|
@ -42,6 +39,7 @@ from noisicaa.builtin_nodes.midi_source import processor_messages as midi_source
|
|||
from . import piano
|
||||
from . import qprogressindicator
|
||||
from . import ui_base
|
||||
from . import instrument_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -57,262 +55,8 @@ logger = logging.getLogger(__name__)
|
|||
# - add/remove
|
||||
|
||||
|
||||
class Item(object):
|
||||
def __init__(self, *, parent: Optional['AbstractFolder']) -> None:
|
||||
self.parent = parent
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return self.key < cast(Item, other).key
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def walk(self) -> Iterator['Item']:
|
||||
yield self
|
||||
|
||||
|
||||
class AbstractFolder(Item): # pylint: disable=abstract-method
|
||||
def __init__(self, *, path: pathlib.Path, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.path = path
|
||||
self.children = [] # type: List[Item]
|
||||
|
||||
def walk(self) -> Iterator['Item']:
|
||||
yield from super().walk()
|
||||
for child in self.children:
|
||||
yield from child.walk()
|
||||
|
||||
|
||||
class Root(AbstractFolder):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(parent=None, **kwargs)
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, str(self.path).lower(), str(self.path))
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return '[root]'
|
||||
|
||||
|
||||
class Folder(AbstractFolder):
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, str(self.path).lower(), str(self.path))
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return str(self.path)
|
||||
|
||||
|
||||
class Instrument(Item):
|
||||
def __init__(
|
||||
self, *, description: instrument_db.InstrumentDescription, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.description = description
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, self.description.display_name.lower(), self.description.uri)
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return self.description.display_name
|
||||
|
||||
|
||||
class LibraryModel(ui_base.CommonMixin, QtCore.QAbstractItemModel):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__root_item = Root(path='/')
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.__root_item = None
|
||||
|
||||
def clear(self) -> None:
|
||||
self.__root_item = Root(path='/')
|
||||
|
||||
def addInstrument(self, description: instrument_db.InstrumentDescription) -> None:
|
||||
parent = self.__root_item # type: AbstractFolder
|
||||
parent_index = self.indexForItem(parent)
|
||||
|
||||
if description.format == instrument_db.InstrumentDescription.SF2:
|
||||
folder_path = pathlib.Path(description.path)
|
||||
else:
|
||||
folder_path = pathlib.Path(description.path).parent
|
||||
|
||||
folder_parts = folder_path.parts
|
||||
assert len(folder_parts) > 0, description.path
|
||||
while folder_parts:
|
||||
for folder_idx, folder in enumerate(parent.children):
|
||||
if not isinstance(folder, AbstractFolder):
|
||||
continue
|
||||
|
||||
match_length = 0
|
||||
for idx, part in enumerate(folder.path.parts):
|
||||
if part != folder_parts[idx]:
|
||||
break
|
||||
match_length += 1
|
||||
|
||||
if match_length > 0:
|
||||
break
|
||||
|
||||
else:
|
||||
folder = Folder(path=pathlib.Path(*folder_parts), parent=parent)
|
||||
match_length = len(folder_parts)
|
||||
folder_idx = bisect.bisect(parent.children, folder)
|
||||
self.beginInsertRows(parent_index, folder_idx, folder_idx)
|
||||
parent.children.insert(folder_idx, folder)
|
||||
self.endInsertRows()
|
||||
|
||||
assert folder_parts[:match_length] == folder.path.parts[:match_length]
|
||||
|
||||
if match_length < len(folder.path.parts):
|
||||
self.beginRemoveRows(parent_index, folder_idx, folder_idx)
|
||||
old_folder = cast(Folder, parent.children.pop(folder_idx))
|
||||
self.endRemoveRows()
|
||||
|
||||
folder = Folder(path=pathlib.Path(*folder_parts[:match_length]), parent=parent)
|
||||
folder_idx = bisect.bisect(parent.children, folder)
|
||||
self.beginInsertRows(parent_index, folder_idx, folder_idx)
|
||||
parent.children.insert(folder_idx, folder)
|
||||
self.endInsertRows()
|
||||
|
||||
new_folder = Folder(path=pathlib.Path(
|
||||
*old_folder.path.parts[match_length:]), parent=folder)
|
||||
for child in old_folder.children:
|
||||
child.parent = new_folder
|
||||
new_folder.children.append(child)
|
||||
new_folder_idx = bisect.bisect(folder.children, folder)
|
||||
self.beginInsertRows(self.indexForItem(folder), new_folder_idx, new_folder_idx)
|
||||
folder.children.insert(folder_idx, new_folder)
|
||||
self.endInsertRows()
|
||||
|
||||
folder_parts = folder_parts[match_length:]
|
||||
|
||||
parent = folder
|
||||
parent_index = self.indexForItem(folder)
|
||||
|
||||
instr = Instrument(
|
||||
description=description,
|
||||
parent=parent)
|
||||
insert_pos = bisect.bisect(parent.children, instr)
|
||||
|
||||
self.beginInsertRows(parent_index, insert_pos, insert_pos)
|
||||
parent.children.insert(insert_pos, instr)
|
||||
self.endInsertRows()
|
||||
|
||||
def instruments(self) -> Iterator[Instrument]:
|
||||
for item in self.__root_item.walk():
|
||||
if isinstance(item, Instrument):
|
||||
yield item
|
||||
|
||||
def flattened(self, parent: Optional[AbstractFolder] = None) -> Iterator[List[str]]:
|
||||
if parent is None:
|
||||
parent = self.__root_item
|
||||
|
||||
path = [] # type: List[str]
|
||||
folder = parent # type: AbstractFolder
|
||||
while folder.parent is not None:
|
||||
path.insert(0, folder.display_name)
|
||||
folder = folder.parent
|
||||
|
||||
if path:
|
||||
yield path
|
||||
|
||||
for item in parent.children:
|
||||
if isinstance(item, Instrument):
|
||||
yield path + [item.display_name]
|
||||
elif isinstance(item, AbstractFolder):
|
||||
yield from self.flattened(item)
|
||||
|
||||
def item(self, index: QtCore.QModelIndex) -> Item:
|
||||
if not index.isValid():
|
||||
raise ValueError("Invalid index")
|
||||
|
||||
item = index.internalPointer()
|
||||
assert item is not None
|
||||
return item
|
||||
|
||||
def indexForItem(self, item: Item, column: int = 0) -> QtCore.QModelIndex:
|
||||
if item.parent is None:
|
||||
return QtCore.QModelIndex()
|
||||
else:
|
||||
return self.createIndex(item.parent.children.index(item), column, item)
|
||||
|
||||
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
if parent.column() > 0: # pragma: no coverage
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
return len(self.__root_item.children)
|
||||
|
||||
parent_item = parent.internalPointer()
|
||||
if parent_item is None:
|
||||
return 0
|
||||
|
||||
if isinstance(parent_item, AbstractFolder):
|
||||
return len(parent_item.children)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
return 1
|
||||
|
||||
def index(
|
||||
self, row: int, column: int = 0, parent: QtCore.QModelIndex = QtCore.QModelIndex()
|
||||
) -> QtCore.QModelIndex:
|
||||
if not self.hasIndex(row, column, parent): # pragma: no coverage
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
return self.createIndex(row, column, self.__root_item.children[row])
|
||||
|
||||
parent_item = parent.internalPointer()
|
||||
assert isinstance(parent_item, AbstractFolder), parent_item.track
|
||||
|
||||
item = parent_item.children[row]
|
||||
return self.createIndex(row, column, item)
|
||||
|
||||
def parent(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: # type: ignore
|
||||
if not index.isValid():
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
item = index.internalPointer()
|
||||
if item is None or item.parent is None:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.indexForItem(item.parent)
|
||||
|
||||
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid(): # pragma: no coverage
|
||||
return None
|
||||
|
||||
item = index.internalPointer()
|
||||
if item is None:
|
||||
return None
|
||||
|
||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||
if index.column() == 0:
|
||||
return item.display_name
|
||||
|
||||
return None # pragma: no coverage
|
||||
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
|
||||
) -> Any: # pragma: no coverage
|
||||
return None
|
||||
|
||||
|
||||
class FilterModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, source: LibraryModel, **kwargs: Any) -> None:
|
||||
def __init__(self, source: instrument_list.InstrumentList, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setSourceModel(source)
|
||||
|
@ -335,10 +79,10 @@ class FilterModel(QtCore.QSortFilterProxyModel):
|
|||
if not parent.isValid():
|
||||
return True
|
||||
|
||||
parent_item = cast(AbstractFolder, self.__source.item(parent))
|
||||
parent_item = cast(instrument_list.AbstractFolder, self.__source.item(parent))
|
||||
item = parent_item.children[row]
|
||||
|
||||
if isinstance(item, AbstractFolder):
|
||||
if isinstance(item, instrument_list.AbstractFolder):
|
||||
# Urgh... this is O(n^2)
|
||||
folder_index = self.__source.index(row, 0, parent)
|
||||
return any(
|
||||
|
@ -389,8 +133,6 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
self.__instrument_id = None # type: str
|
||||
self.__midi_source_id = None # type: str
|
||||
|
||||
self.__instrument_mutation_listener = None # type: core.Listener
|
||||
|
||||
self.__instrument = None # type: instrument_db.InstrumentDescription
|
||||
self.__instrument_loader_task = None # type: asyncio.Task
|
||||
self.__instrument_queue = asyncio.Queue(loop=self.event_loop) # type: asyncio.Queue
|
||||
|
@ -426,8 +168,7 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
self.instruments_search.addAction(clear_action, QtWidgets.QLineEdit.TrailingPosition)
|
||||
self.instruments_search.textChanged.connect(self.onInstrumentSearchChanged)
|
||||
|
||||
self.__model = LibraryModel(context=self.context)
|
||||
self.__model_filter = FilterModel(self.__model)
|
||||
self.__model_filter = FilterModel(self.app.instrument_list)
|
||||
self.__view = LibraryView(self)
|
||||
self.__view.setModel(self.__model_filter)
|
||||
layout.addWidget(self.__view, 1)
|
||||
|
@ -508,13 +249,6 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
async def setup(self) -> None:
|
||||
logger.info("Setting up instrument library dialog...")
|
||||
|
||||
self.__model.clear()
|
||||
for description in self.app.instrument_db.instruments:
|
||||
self.__model.addInstrument(description)
|
||||
|
||||
self.__instrument_mutation_listener = self.app.instrument_db.mutation_handlers.add(
|
||||
self.handleInstrumentMutation)
|
||||
|
||||
self.__instrument_id = uuid.uuid4().hex
|
||||
await self.audioproc_client.add_node(
|
||||
'root',
|
||||
|
@ -574,17 +308,6 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
'root', self.__instrument_id)
|
||||
self.__instrument_id = None
|
||||
|
||||
if self.__instrument_mutation_listener is not None:
|
||||
self.__instrument_mutation_listener.remove()
|
||||
self.__instrument_mutation_listener = None
|
||||
|
||||
def handleInstrumentMutation(self, mutation: instrument_db.Mutation) -> None:
|
||||
logger.info("Mutation received: %s", mutation)
|
||||
if mutation.WhichOneof('type') == 'add_instrument':
|
||||
self.__model.addInstrument(mutation.add_instrument)
|
||||
else:
|
||||
raise TypeError(type(mutation))
|
||||
|
||||
async def __instrumentLoader(self) -> None:
|
||||
while True:
|
||||
description = await self.__instrument_queue.get()
|
||||
|
@ -624,16 +347,16 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
self.call_async(self.app.instrument_db.start_scan())
|
||||
|
||||
def selectInstrument(self, uri: str) -> None:
|
||||
for instr in self.__model.instruments():
|
||||
for instr in self.app.instrument_list.instruments():
|
||||
if instr.description.uri == uri:
|
||||
index = self.__model.indexForItem(instr)
|
||||
index = self.app.instrument_list.indexForItem(instr)
|
||||
index = self.__model_filter.mapFromSource(index)
|
||||
self.__view.setCurrentIndex(index)
|
||||
|
||||
def onInstrumentItemSelected(self, index: QtCore.QModelIndex) -> None:
|
||||
index = self.__model_filter.mapToSource(index)
|
||||
item = self.__model.item(index)
|
||||
if item is not None and isinstance(item, Instrument):
|
||||
item = self.app.instrument_list.item(index)
|
||||
if item is not None and isinstance(item, instrument_list.Instrument):
|
||||
self.__instrument_queue.put_nowait(item.description)
|
||||
|
||||
def onInstrumentSearchChanged(self, text: str) -> None:
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
# @end:license
|
||||
|
||||
import bisect
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import cast, Any, Optional, Iterator, List, Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from noisicaa import instrument_db
|
||||
from noisicaa import core
|
||||
from . import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Item(object):
|
||||
def __init__(self, *, parent: Optional['AbstractFolder']) -> None:
|
||||
self.parent = parent
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return self.key < cast(Item, other).key
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def walk(self) -> Iterator['Item']:
|
||||
yield self
|
||||
|
||||
|
||||
class AbstractFolder(Item): # pylint: disable=abstract-method
|
||||
def __init__(self, *, path: pathlib.Path, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.path = path
|
||||
self.children = [] # type: List[Item]
|
||||
|
||||
def walk(self) -> Iterator['Item']:
|
||||
yield from super().walk()
|
||||
for child in self.children:
|
||||
yield from child.walk()
|
||||
|
||||
|
||||
class Root(AbstractFolder):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(parent=None, **kwargs)
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, str(self.path).lower(), str(self.path))
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return '[root]'
|
||||
|
||||
|
||||
class Folder(AbstractFolder):
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, str(self.path).lower(), str(self.path))
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return str(self.path)
|
||||
|
||||
|
||||
class Instrument(Item):
|
||||
def __init__(
|
||||
self, *, description: instrument_db.InstrumentDescription, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.description = description
|
||||
|
||||
@property
|
||||
def key(self) -> Tuple[int, str, str]:
|
||||
return (0, self.description.display_name.lower(), self.description.uri)
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return self.description.display_name
|
||||
|
||||
|
||||
class InstrumentList(ui_base.CommonMixin, QtCore.QAbstractItemModel):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__instrument_mutation_listener = None # type: core.Listener
|
||||
|
||||
self.__root_item = Root(path='/')
|
||||
|
||||
def setup(self) -> None:
|
||||
for description in self.app.instrument_db.instruments:
|
||||
self.__addInstrument(description)
|
||||
|
||||
self.__instrument_mutation_listener = self.app.instrument_db.mutation_handlers.add(
|
||||
self.__handleInstrumentMutation)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.__root_item = None
|
||||
|
||||
if self.__instrument_mutation_listener is not None:
|
||||
self.__instrument_mutation_listener.remove()
|
||||
self.__instrument_mutation_listener = None
|
||||
|
||||
def clear(self) -> None:
|
||||
self.__root_item = Root(path='/')
|
||||
|
||||
def __handleInstrumentMutation(self, mutation: instrument_db.Mutation) -> None:
|
||||
if mutation.WhichOneof('type') == 'add_instrument':
|
||||
self.addInstrument(mutation.add_instrument)
|
||||
else:
|
||||
raise TypeError(type(mutation))
|
||||
|
||||
def addInstrument(self, description: instrument_db.InstrumentDescription) -> None:
|
||||
parent = self.__root_item # type: AbstractFolder
|
||||
parent_index = self.indexForItem(parent)
|
||||
|
||||
if description.format == instrument_db.InstrumentDescription.SF2:
|
||||
folder_path = pathlib.Path(description.path)
|
||||
else:
|
||||
folder_path = pathlib.Path(description.path).parent
|
||||
|
||||
folder_parts = folder_path.parts
|
||||
assert len(folder_parts) > 0, description.path
|
||||
while folder_parts:
|
||||
for folder_idx, folder in enumerate(parent.children):
|
||||
if not isinstance(folder, AbstractFolder):
|
||||
continue
|
||||
|
||||
match_length = 0
|
||||
for idx, part in enumerate(folder.path.parts):
|
||||
if part != folder_parts[idx]:
|
||||
break
|
||||
match_length += 1
|
||||
|
||||
if match_length > 0:
|
||||
break
|
||||
|
||||
else:
|
||||
folder = Folder(path=pathlib.Path(*folder_parts), parent=parent)
|
||||
match_length = len(folder_parts)
|
||||
folder_idx = bisect.bisect(parent.children, folder)
|
||||
self.beginInsertRows(parent_index, folder_idx, folder_idx)
|
||||
parent.children.insert(folder_idx, folder)
|
||||
self.endInsertRows()
|
||||
|
||||
assert folder_parts[:match_length] == folder.path.parts[:match_length]
|
||||
|
||||
if match_length < len(folder.path.parts):
|
||||
self.beginRemoveRows(parent_index, folder_idx, folder_idx)
|
||||
old_folder = cast(Folder, parent.children.pop(folder_idx))
|
||||
self.endRemoveRows()
|
||||
|
||||
folder = Folder(path=pathlib.Path(*folder_parts[:match_length]), parent=parent)
|
||||
folder_idx = bisect.bisect(parent.children, folder)
|
||||
self.beginInsertRows(parent_index, folder_idx, folder_idx)
|
||||
parent.children.insert(folder_idx, folder)
|
||||
self.endInsertRows()
|
||||
|
||||
new_folder = Folder(path=pathlib.Path(
|
||||
*old_folder.path.parts[match_length:]), parent=folder)
|
||||
for child in old_folder.children:
|
||||
child.parent = new_folder
|
||||
new_folder.children.append(child)
|
||||
new_folder_idx = bisect.bisect(folder.children, folder)
|
||||
self.beginInsertRows(self.indexForItem(folder), new_folder_idx, new_folder_idx)
|
||||
folder.children.insert(folder_idx, new_folder)
|
||||
self.endInsertRows()
|
||||
|
||||
folder_parts = folder_parts[match_length:]
|
||||
|
||||
parent = folder
|
||||
parent_index = self.indexForItem(folder)
|
||||
|
||||
instr = Instrument(
|
||||
description=description,
|
||||
parent=parent)
|
||||
insert_pos = bisect.bisect(parent.children, instr)
|
||||
|
||||
self.beginInsertRows(parent_index, insert_pos, insert_pos)
|
||||
parent.children.insert(insert_pos, instr)
|
||||
self.endInsertRows()
|
||||
|
||||
def instruments(self) -> Iterator[Instrument]:
|
||||
for item in self.__root_item.walk():
|
||||
if isinstance(item, Instrument):
|
||||
yield item
|
||||
|
||||
def flattened(self, parent: Optional[AbstractFolder] = None) -> Iterator[List[str]]:
|
||||
if parent is None:
|
||||
parent = self.__root_item
|
||||
|
||||
path = [] # type: List[str]
|
||||
folder = parent # type: AbstractFolder
|
||||
while folder.parent is not None:
|
||||
path.insert(0, folder.display_name)
|
||||
folder = folder.parent
|
||||
|
||||
if path:
|
||||
yield path
|
||||
|
||||
for item in parent.children:
|
||||
if isinstance(item, Instrument):
|
||||
yield path + [item.display_name]
|
||||
elif isinstance(item, AbstractFolder):
|
||||
yield from self.flattened(item)
|
||||
|
||||
def item(self, index: QtCore.QModelIndex) -> Item:
|
||||
if not index.isValid():
|
||||
raise ValueError("Invalid index")
|
||||
|
||||
item = index.internalPointer()
|
||||
assert item is not None
|
||||
return item
|
||||
|
||||
def indexForItem(self, item: Item, column: int = 0) -> QtCore.QModelIndex:
|
||||
if item.parent is None:
|
||||
return QtCore.QModelIndex()
|
||||
else:
|
||||
return self.createIndex(item.parent.children.index(item), column, item)
|
||||
|
||||
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
if parent.column() > 0: # pragma: no coverage
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
return len(self.__root_item.children)
|
||||
|
||||
parent_item = parent.internalPointer()
|
||||
if parent_item is None:
|
||||
return 0
|
||||
|
||||
if isinstance(parent_item, AbstractFolder):
|
||||
return len(parent_item.children)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
return 1
|
||||
|
||||
def index(
|
||||
self, row: int, column: int = 0, parent: QtCore.QModelIndex = QtCore.QModelIndex()
|
||||
) -> QtCore.QModelIndex:
|
||||
if not self.hasIndex(row, column, parent): # pragma: no coverage
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
return self.createIndex(row, column, self.__root_item.children[row])
|
||||
|
||||
parent_item = parent.internalPointer()
|
||||
assert isinstance(parent_item, AbstractFolder), parent_item.track
|
||||
|
||||
item = parent_item.children[row]
|
||||
return self.createIndex(row, column, item)
|
||||
|
||||
def parent(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: # type: ignore
|
||||
if not index.isValid():
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
item = index.internalPointer()
|
||||
if item is None or item.parent is None:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.indexForItem(item.parent)
|
||||
|
||||
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid(): # pragma: no coverage
|
||||
return None
|
||||
|
||||
item = index.internalPointer()
|
||||
if item is None:
|
||||
return None
|
||||
|
||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||
if index.column() == 0:
|
||||
return item.display_name
|
||||
|
||||
return None # pragma: no coverage
|
||||
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
|
||||
) -> Any: # pragma: no coverage
|
||||
return None
|
|
@ -27,10 +27,10 @@ from PyQt5 import QtCore
|
|||
|
||||
from noisidev import uitest
|
||||
from noisicaa import instrument_db
|
||||
from . import instrument_library
|
||||
from . import instrument_list
|
||||
|
||||
|
||||
class TracksModelTest(uitest.UITestCase):
|
||||
class InstrumentListTest(uitest.UITestCase):
|
||||
def __mkinstr(self, path):
|
||||
return instrument_db.InstrumentDescription(
|
||||
uri='wav:' + path,
|
||||
|
@ -38,7 +38,7 @@ class TracksModelTest(uitest.UITestCase):
|
|||
display_name=os.path.splitext(os.path.basename(path))[0])
|
||||
|
||||
async def test_addInstrument(self):
|
||||
model = instrument_library.LibraryModel(context=self.context)
|
||||
model = instrument_list.InstrumentList(context=self.context)
|
||||
try:
|
||||
root_index = QtCore.QModelIndex()
|
||||
self.assertEqual(model.rowCount(root_index), 0)
|
||||
|
@ -61,7 +61,7 @@ class TracksModelTest(uitest.UITestCase):
|
|||
model.cleanup()
|
||||
|
||||
async def test_addInstrument_long_path(self):
|
||||
model = instrument_library.LibraryModel(context=self.context)
|
||||
model = instrument_list.InstrumentList(context=self.context)
|
||||
try:
|
||||
model.addInstrument(self.__mkinstr('/some/path/test1.wav'))
|
||||
self.assertEqual(
|
|
@ -41,6 +41,7 @@ if typing.TYPE_CHECKING:
|
|||
from noisicaa import node_db as node_db_lib
|
||||
from noisicaa import runtime_settings as runtime_settings_lib
|
||||
from noisicaa import lv2
|
||||
from . import instrument_list as instrument_list_lib
|
||||
from . import project_registry
|
||||
|
||||
|
||||
|
@ -263,6 +264,7 @@ class AbstractEditorApp(object):
|
|||
default_style = None # type: str
|
||||
qt_app = None # type: QtWidgets.QApplication
|
||||
devices = None # type: device_list.DeviceList
|
||||
instrument_list = None # type: instrument_list_lib.InstrumentList
|
||||
|
||||
def quit(self, exit_code: int = 0) -> None:
|
||||
raise NotImplementedError
|
||||
|
|
Loading…
Reference in New Issue