An open source DAW for GNU/Linux, inspired by modular synths.
http://noisicaa.odahoda.de/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
10 KiB
320 lines
10 KiB
#!/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 logging |
|
import os |
|
import os.path |
|
import shutil |
|
import time |
|
import urllib.parse |
|
from typing import cast, Any, List, Iterable, Iterator |
|
|
|
from PyQt5.QtCore import Qt |
|
from PyQt5 import QtCore |
|
|
|
from noisicaa.core.typing_extra import down_cast |
|
from noisicaa import music |
|
from . import ui_base |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class Item(QtCore.QObject): |
|
contentsChanged = QtCore.pyqtSignal() |
|
|
|
PathRole = 0x0100 |
|
|
|
def __init__(self, *, path: str) -> None: |
|
super().__init__() |
|
|
|
self.path = path |
|
self.index = 0 |
|
self.childItems = [] # type: List[Item] |
|
|
|
def isOpened(self) -> bool: |
|
return False |
|
|
|
def projects(self) -> Iterator['Project']: |
|
for child in self.childItems: |
|
yield from child.projects() |
|
|
|
def data(self, role: int) -> Any: |
|
if role == Item.PathRole: |
|
return self.path |
|
|
|
return None |
|
|
|
def flags(self) -> Qt.ItemFlags: |
|
return cast(Qt.ItemFlags, Qt.NoItemFlags) |
|
|
|
|
|
class Root(Item): |
|
def __init__(self) -> None: |
|
super().__init__(path='<root>') |
|
|
|
|
|
class Project(ui_base.CommonMixin, Item): |
|
NameRole = 0x0101 |
|
MTimeRole = 0x0102 |
|
|
|
def __init__(self, **kwargs: Any) -> None: |
|
super().__init__(**kwargs) |
|
|
|
self.__debuggerRunning = False |
|
|
|
self.client = None # type: music.ProjectClient |
|
if os.path.isfile(os.path.join(self.path, 'project.noise')): |
|
self.__mtime = os.path.getmtime(os.path.join(self.path, 'project.noise')) |
|
else: |
|
self.__mtime = time.time() |
|
|
|
def projects(self) -> Iterator['Project']: |
|
yield self |
|
yield from super().projects() |
|
|
|
def flags(self) -> Qt.ItemFlags: |
|
return cast(Qt.ItemFlags, Qt.ItemIsSelectable | Qt.ItemIsEnabled) |
|
|
|
def data(self, role: int) -> Any: |
|
if role == Project.NameRole: |
|
return self.name |
|
|
|
if role == Project.MTimeRole: |
|
return self.mtime |
|
|
|
return super().data(role) |
|
|
|
def isOpened(self) -> bool: |
|
return self.client is not None or self.__debuggerRunning |
|
|
|
@property |
|
def name(self) -> str: |
|
return urllib.parse.unquote(os.path.splitext(os.path.basename(self.path))[0]) |
|
|
|
@property |
|
def mtime(self) -> float: |
|
return self.__mtime |
|
|
|
def startDebugger(self) -> None: |
|
self.__debuggerRunning = True |
|
self.contentsChanged.emit() |
|
|
|
def endDebugger(self) -> None: |
|
self.__debuggerRunning = False |
|
self.contentsChanged.emit() |
|
|
|
async def __create_process(self) -> music.ProjectClient: |
|
client = music.ProjectClient( |
|
event_loop=self.event_loop, |
|
server=self.app.process.server, |
|
node_db=self.app.node_db, |
|
urid_mapper=self.app.urid_mapper, |
|
manager=self.app.process.manager, |
|
tmp_dir=self.app.process.tmp_dir, |
|
) |
|
await client.setup() |
|
return client |
|
|
|
async def open(self) -> None: |
|
assert not self.isOpened() |
|
client = await self.__create_process() |
|
try: |
|
await client.open(self.path) |
|
except: # pylint: disable=bare-except |
|
await client.cleanup() |
|
raise |
|
self.client = client |
|
self.app.project_registry.updateOpenedProjects() |
|
self.__mtime = time.time() |
|
self.contentsChanged.emit() |
|
|
|
async def create(self) -> None: |
|
assert not self.isOpened() |
|
client = await self.__create_process() |
|
try: |
|
await client.create(self.path) |
|
except: # pylint: disable=bare-except |
|
await client.cleanup() |
|
raise |
|
self.client = client |
|
self.app.project_registry.addProject(self) |
|
self.app.project_registry.updateOpenedProjects() |
|
self.__mtime = time.time() |
|
self.contentsChanged.emit() |
|
|
|
async def close(self) -> None: |
|
if self.client is not None: |
|
await self.client.close() |
|
await self.client.cleanup() |
|
self.client = None |
|
self.app.project_registry.updateOpenedProjects() |
|
self.__mtime = time.time() |
|
self.contentsChanged.emit() |
|
|
|
async def delete(self) -> None: |
|
assert not self.isOpened() |
|
shutil.rmtree(self.path) |
|
|
|
|
|
class ProjectRegistry(ui_base.CommonMixin, QtCore.QAbstractItemModel): |
|
contentsChanged = QtCore.pyqtSignal() |
|
|
|
def __init__(self, **kwargs: Any) -> None: |
|
super().__init__(**kwargs) |
|
|
|
self.__root = Root() |
|
self.__in_cleanup = False |
|
|
|
async def setup(self) -> None: |
|
await self.refresh() |
|
|
|
async def cleanup(self) -> None: |
|
self.__in_cleanup = True |
|
for project in self.__root.projects(): |
|
await project.close() |
|
self.__root = None |
|
|
|
def __scan_projects(self, directories: Iterable[str]) -> List[str]: |
|
paths = [] # type: List[str] |
|
for directory in directories: |
|
directory = os.path.expanduser(directory) |
|
directory = os.path.abspath(directory) |
|
logger.info("Scanning project directory %s...", directory) |
|
for dirpath, dirnames, _ in os.walk(directory): |
|
for dirname in list(dirnames): |
|
if os.path.isfile(os.path.join(dirpath, dirname, 'project.noise')): |
|
dirnames.remove(dirname) |
|
paths.append(os.path.join(dirpath, dirname)) |
|
|
|
return paths |
|
|
|
def projects(self) -> List[Project]: |
|
return list(self.__root.projects()) |
|
|
|
async def refresh(self) -> None: |
|
directories = self.app.settings.value('project_folders', ['~/Music/Noisicaä']) |
|
|
|
paths = await self.event_loop.run_in_executor(None, self.__scan_projects, directories) |
|
|
|
old_paths = {project.path for project in self.__root.projects()} |
|
new_paths = set(paths) |
|
|
|
for path in old_paths - new_paths: |
|
logger.info("Removing project at %s...", path) |
|
|
|
for idx, project in enumerate(self.__root.childItems): |
|
if project.path == path: |
|
self.beginRemoveRows(QtCore.QModelIndex(), idx, idx) |
|
del self.__root.childItems[idx] |
|
for i, item in enumerate(self.__root.childItems[idx:], idx): |
|
item.index = i |
|
self.endRemoveRows() |
|
await project.close() |
|
self.contentsChanged.emit() |
|
break |
|
|
|
for path in new_paths - old_paths: |
|
logger.info("Adding project at %s...", path) |
|
project = Project(path=path, context=self.context) |
|
self.addProject(project) |
|
|
|
def getProject(self, path: str) -> Project: |
|
path = os.path.expanduser(path) |
|
path = os.path.abspath(path) |
|
for project in self.projects(): |
|
if project.path == path: |
|
return project |
|
raise KeyError("No known project at '%s'" % path) |
|
|
|
def addProject(self, project: Project) -> None: |
|
idx = 0 |
|
while idx < len(self.__root.childItems) and project.path > self.__root.childItems[idx].path: |
|
idx += 1 |
|
|
|
project.setParent(self.__root) |
|
|
|
self.beginInsertRows(QtCore.QModelIndex(), idx, idx) |
|
self.__root.childItems.insert(idx, project) |
|
for idx, item in enumerate(self.__root.childItems[idx:], idx): |
|
item.index = idx |
|
self.endInsertRows() |
|
|
|
project.contentsChanged.connect(lambda: self.__projectChanged(project)) |
|
project.contentsChanged.connect(self.contentsChanged.emit) |
|
self.contentsChanged.emit() |
|
|
|
def updateOpenedProjects(self) -> None: |
|
if self.__in_cleanup: |
|
return |
|
opened_project_paths = sorted( |
|
project.path for project in self.projects() if project.isOpened()) |
|
logger.info("Currently opened projects:\n%s", '\n'.join(opened_project_paths)) |
|
self.app.settings.setValue('opened_projects', opened_project_paths) |
|
|
|
def __projectChanged(self, project: Project) -> None: |
|
index = self.index(project.index) |
|
self.dataChanged.emit(index, index) |
|
|
|
def item(self, index: QtCore.QModelIndex = QtCore.QModelIndex()) -> Item: |
|
if not index.isValid(): |
|
return self.__root |
|
else: |
|
return down_cast(Item, index.internalPointer()) |
|
|
|
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() |
|
|
|
parent_item = self.item(parent) |
|
return self.createIndex(row, column, parent_item.childItems[row]) |
|
|
|
def parent(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: # type: ignore |
|
if not index.isValid(): |
|
return QtCore.QModelIndex() |
|
|
|
item = down_cast(Item, index.internalPointer()) |
|
if item is self.__root: |
|
return QtCore.QModelIndex() |
|
|
|
return self.createIndex(item.parent().index, 0, item.parent()) |
|
|
|
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: |
|
return 1 |
|
|
|
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: |
|
parent_item = self.item(parent) |
|
if parent_item is None: |
|
return 0 |
|
return len(parent_item.childItems) |
|
|
|
def flags(self, index: QtCore.QModelIndex) -> Qt.ItemFlags: |
|
return self.item(index).flags() |
|
|
|
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> Any: |
|
return self.item(index).data(role) |
|
|
|
def headerData( |
|
self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole |
|
) -> Any: # pragma: no coverage |
|
return None
|
|
|