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.
559 lines
21 KiB
559 lines
21 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 datetime |
|
import functools |
|
import logging |
|
import os.path |
|
import random |
|
from typing import Any, Dict, List |
|
|
|
from PyQt5.QtCore import Qt |
|
from PyQt5 import QtCore |
|
from PyQt5 import QtGui |
|
from PyQt5 import QtWidgets |
|
import humanize |
|
|
|
from noisicaa import title_generator |
|
from . import project_registry as project_registry_lib |
|
from . import slots |
|
from . import ui_base |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class ProjectItem(slots.SlotContainer, QtWidgets.QWidget): |
|
def __init__(self, project: project_registry_lib.Project) -> None: |
|
super().__init__() |
|
|
|
self.__project = project |
|
|
|
self.setAutoFillBackground(True) |
|
|
|
self.__opened = QtWidgets.QLabel(self) |
|
self.__opened.setText("[open]") |
|
opened_font = QtGui.QFont(self.__opened.font()) |
|
opened_font.setPointSizeF(1.4 * opened_font.pointSizeF()) |
|
opened_font.setBold(True) |
|
self.__opened.setFont(opened_font) |
|
opened_palette = QtGui.QPalette(self.__opened.palette()) |
|
opened_palette.setColor(QtGui.QPalette.Text, Qt.red) |
|
opened_palette.setColor(QtGui.QPalette.HighlightedText, Qt.red) |
|
self.__opened.setPalette(opened_palette) |
|
|
|
self.__name = QtWidgets.QLabel(self) |
|
name_font = QtGui.QFont(self.__name.font()) |
|
name_font.setPointSizeF(1.4 * name_font.pointSizeF()) |
|
name_font.setBold(True) |
|
self.__name.setFont(name_font) |
|
|
|
self.__path = QtWidgets.QLabel(self) |
|
self.__mtime = QtWidgets.QLabel(self) |
|
|
|
l2 = QtWidgets.QHBoxLayout() |
|
l2.setContentsMargins(0, 0, 0, 0) |
|
l2.addWidget(self.__opened) |
|
l2.addWidget(self.__name, 1) |
|
|
|
l1 = QtWidgets.QVBoxLayout() |
|
l1.setContentsMargins(0, 0, 0, 0) |
|
l1.addLayout(l2) |
|
l1.addWidget(self.__path) |
|
l1.addWidget(self.__mtime) |
|
self.setLayout(l1) |
|
|
|
def updateContents(self) -> None: |
|
self.__opened.setVisible(self.__project.isOpened()) |
|
self.__name.setText(self.__project.name) |
|
self.__path.setText(self.__project.path) |
|
self.__mtime.setText( |
|
'Last usage: %s' % humanize.naturaltime( |
|
datetime.datetime.fromtimestamp(self.__project.mtime))) |
|
|
|
|
|
class ItemDelegate(QtWidgets.QAbstractItemDelegate): |
|
def __init__(self) -> None: |
|
super().__init__() |
|
|
|
self.__widgets = {} # type: Dict[str, QtWidgets.QWidget] |
|
|
|
def __getWidget(self, index: QtCore.QModelIndex) -> QtWidgets.QWidget: |
|
item = index.model().item(index) |
|
if item is None: |
|
logger.error("Index without item: %d,%d", index.row(), index.column()) |
|
path = '<invalid>' |
|
else: |
|
path = item.path |
|
|
|
if path not in self.__widgets: |
|
widget = None # type: QtWidgets.QWidget |
|
if isinstance(item, project_registry_lib.Project): |
|
widget = ProjectItem(item) |
|
elif isinstance(item, project_registry_lib.Root) or item is None: |
|
widget = QtWidgets.QWidget() |
|
else: |
|
raise TypeError("%s: %s" % (path, type(item))) |
|
self.__widgets[path] = widget |
|
|
|
return self.__widgets[path] |
|
|
|
def paint( |
|
self, |
|
painter: QtGui.QPainter, |
|
option: QtWidgets.QStyleOptionViewItem, |
|
index: QtCore.QModelIndex |
|
) -> None: |
|
widget = self.__getWidget(index) |
|
widget.resize(option.rect.size()) |
|
widget.updateContents() |
|
if option.state & QtWidgets.QStyle.State_Selected: |
|
widget.setBackgroundRole(QtGui.QPalette.Highlight) |
|
else: |
|
widget.setBackgroundRole(QtGui.QPalette.Base) |
|
|
|
# Why do I have to render to a pixmap first? QWidget.render() should be able to directly |
|
# render into a QPainter... |
|
pixmap = QtGui.QPixmap(option.rect.size()) |
|
widget.render(pixmap) |
|
painter.drawPixmap(option.rect, pixmap) |
|
|
|
def sizeHint( |
|
self, |
|
option: QtWidgets.QStyleOptionViewItem, |
|
index: QtCore.QModelIndex |
|
) -> QtCore.QSize: |
|
widget = self.__getWidget(index) |
|
return widget.sizeHint() |
|
|
|
|
|
class ProjectListView(QtWidgets.QListView): |
|
numProjectsSelected = QtCore.pyqtSignal(int) |
|
itemDoubleClicked = QtCore.pyqtSignal(project_registry_lib.Item) |
|
|
|
def __init__(self, parent: QtWidgets.QWidget) -> None: |
|
super().__init__(parent) |
|
|
|
self.__delegate = ItemDelegate() |
|
self.setItemDelegate(self.__delegate) |
|
|
|
self.setSpacing(4) |
|
|
|
self.doubleClicked.connect(self.__doubleClicked) |
|
|
|
self.__update_timer = QtCore.QTimer(self) |
|
self.__update_timer.timeout.connect(lambda: self.viewport().update()) |
|
self.__update_timer.setInterval(300) |
|
self.__update_timer.start() |
|
|
|
def __doubleClicked(self, index: QtCore.QModelIndex) -> None: |
|
item = self.model().item(index) |
|
self.itemDoubleClicked.emit(item) |
|
|
|
def selectionChanged( |
|
self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection) -> None: |
|
self.numProjectsSelected.emit(len(self.selectedIndexes())) |
|
|
|
def selectedProjects(self) -> List[project_registry_lib.Project]: |
|
projects = [] |
|
for index in self.selectedIndexes(): |
|
item = self.model().item(index) |
|
if isinstance(item, project_registry_lib.Project): |
|
projects.append(item) |
|
|
|
return projects |
|
|
|
|
|
class FlatProjectListModel(QtCore.QAbstractProxyModel): |
|
def __init__(self, project_registry: project_registry_lib.ProjectRegistry) -> None: |
|
super().__init__() |
|
|
|
self.__registry = project_registry |
|
self.setSourceModel(self.__registry) |
|
|
|
self.__registry.rowsAboutToBeInserted.connect( |
|
lambda parent, r1, r2: self.beginInsertRows(self.mapFromSource(parent), r1, r2)) |
|
self.__registry.rowsInserted.connect(self.endInsertRows) |
|
self.__registry.rowsAboutToBeRemoved.connect( |
|
lambda parent, r1, r2: self.beginRemoveRows(self.mapFromSource(parent), r1, r2)) |
|
self.__registry.rowsRemoved.connect(self.endRemoveRows) |
|
self.__registry.dataChanged.connect( |
|
lambda topLeft, bottomRight, roles: self.dataChanged.emit( |
|
self.index(0), self.index(self.rowCount() - 1), roles)) |
|
|
|
self.__root = QtCore.QModelIndex() |
|
|
|
def item(self, index: QtCore.QModelIndex = QtCore.QModelIndex()) -> project_registry_lib.Item: |
|
return self.__registry.item(self.mapToSource(index)) |
|
|
|
def mapFromSource(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: |
|
if not index.isValid(): |
|
return QtCore.QModelIndex() |
|
return self.createIndex(index.row(), index.column(), QtCore.QModelIndex()) |
|
|
|
def mapToSource(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: |
|
return self.__registry.index(index.row(), index.column(), self.__root) |
|
|
|
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: |
|
return self.__registry.rowCount(self.__root) |
|
|
|
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: |
|
return self.__registry.columnCount(self.__root) |
|
|
|
def index( |
|
self, row: int, column: int = 0, parent: QtCore.QModelIndex = QtCore.QModelIndex() |
|
) -> QtCore.QModelIndex: |
|
return self.createIndex(row, column, None) |
|
|
|
def parent(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex: # type: ignore |
|
return QtCore.QModelIndex() |
|
|
|
|
|
class FilterModel(QtCore.QSortFilterProxyModel): |
|
def __init__(self) -> None: |
|
super().__init__() |
|
|
|
self.__filter = [] # type: List[str] |
|
|
|
def item(self, index: QtCore.QModelIndex = QtCore.QModelIndex()) -> project_registry_lib.Item: |
|
source_model = self.sourceModel() |
|
return source_model.item(self.mapToSource(index)) |
|
|
|
def setFilterWords(self, text: str) -> None: |
|
words = text.split() |
|
words = [word.strip() for word in words] |
|
words = [word for word in words if word] |
|
words = [word.lower() for word in words] |
|
self.__filter = words |
|
self.invalidateFilter() |
|
|
|
def filterAcceptsRow(self, row: int, parent: QtCore.QModelIndex) -> bool: |
|
if not self.__filter: |
|
return True |
|
|
|
model = self.sourceModel() |
|
parent_item = model.item(parent) |
|
item = parent_item.children[row] |
|
|
|
return all(word in item.name.lower() for word in self.__filter) |
|
|
|
|
|
class NewProjectDialog(ui_base.CommonMixin, QtWidgets.QDialog): |
|
def __init__(self, **kwargs: Any) -> None: |
|
super().__init__(**kwargs) |
|
|
|
self.setWindowTitle("noisicaä - New Project") |
|
self.setMinimumWidth(500) |
|
|
|
self.__create_button = QtWidgets.QPushButton(self) |
|
self.__create_button.setIcon(QtGui.QIcon.fromTheme('document-new')) |
|
self.__create_button.setText("Create") |
|
self.__create_button.clicked.connect(self.accept) |
|
|
|
self.__close_button = QtWidgets.QPushButton(self) |
|
self.__close_button.setIcon(QtGui.QIcon.fromTheme('window-close')) |
|
self.__close_button.setText("Cancel") |
|
self.__close_button.clicked.connect(self.reject) |
|
|
|
self.__name = QtWidgets.QLineEdit(self) |
|
self.__name.textChanged.connect(self.__nameChanged) |
|
|
|
self.__error = QtWidgets.QLabel(self) |
|
palette = QtGui.QPalette(self.__error.palette()) |
|
palette.setColor(QtGui.QPalette.WindowText, Qt.red) |
|
self.__error.setPalette(palette) |
|
|
|
self.__prev_name = QtWidgets.QToolButton(self) |
|
self.__prev_name.setIcon(QtGui.QIcon.fromTheme('go-previous')) |
|
self.__prev_name.clicked.connect(self.__prevNameClicked) |
|
|
|
self.__next_name = QtWidgets.QToolButton(self) |
|
self.__next_name.setIcon(QtGui.QIcon.fromTheme('go-next')) |
|
self.__next_name.clicked.connect(self.__nextNameClicked) |
|
|
|
self.__name_seed = random.randint(0, 1000000) |
|
self.__generateName() |
|
self.__name.setFocus() |
|
self.__name.selectAll() |
|
|
|
l4 = QtWidgets.QHBoxLayout() |
|
l4.addWidget(self.__name, 1) |
|
l4.addWidget(self.__prev_name) |
|
l4.addWidget(self.__next_name) |
|
|
|
l3 = QtWidgets.QFormLayout() |
|
l3.addRow("Name:", l4) |
|
|
|
l2 = QtWidgets.QHBoxLayout() |
|
l2.addStretch(1) |
|
l2.addWidget(self.__create_button) |
|
l2.addWidget(self.__close_button) |
|
|
|
l1 = QtWidgets.QVBoxLayout() |
|
l1.addLayout(l3) |
|
l1.addWidget(self.__error) |
|
l1.addLayout(l2) |
|
self.setLayout(l1) |
|
|
|
def projectDir(self) -> str: |
|
directory = '~/Music/Noisicaä' |
|
directory = os.path.expanduser(directory) |
|
directory = os.path.abspath(directory) |
|
return directory |
|
|
|
def projectName(self) -> str: |
|
return self.__name.text() |
|
|
|
def projectPath(self) -> str: |
|
filename = self.projectName() |
|
filename = filename.replace('%', '%25') |
|
filename = filename.replace('/', '%2F') |
|
return os.path.join(self.projectDir(), filename) |
|
|
|
def __generateName(self) -> None: |
|
gen = title_generator.TitleGenerator(self.__name_seed) |
|
self.__name.setText(gen.generate()) |
|
|
|
def __nextNameClicked(self) -> None: |
|
self.__name_seed = (self.__name_seed + 1) % 1000000 |
|
self.__generateName() |
|
|
|
def __prevNameClicked(self) -> None: |
|
self.__name_seed = (self.__name_seed - 1) % 1000000 |
|
self.__generateName() |
|
|
|
def __nameChanged(self, text: str) -> None: |
|
self.__create_button.setEnabled(False) |
|
self.__error.setVisible(True) |
|
if not text: |
|
self.__error.setText("Enter a valid project name.") |
|
elif os.path.exists(self.projectPath()): |
|
self.__error.setText("A project of this name already exists.") |
|
else: |
|
self.__create_button.setEnabled(True) |
|
self.__error.setVisible(False) |
|
|
|
|
|
class OpenProjectDialog(ui_base.CommonMixin, QtWidgets.QWidget): |
|
projectSelected = QtCore.pyqtSignal(project_registry_lib.Project) |
|
createProject = QtCore.pyqtSignal(str) |
|
debugProject = QtCore.pyqtSignal(project_registry_lib.Project) |
|
|
|
def __init__(self, **kwargs: Any) -> None: |
|
super().__init__(**kwargs) |
|
|
|
self.setObjectName('open-project-dialog') |
|
|
|
self.__filter_model = FilterModel() |
|
self.__filter_model.setSourceModel(FlatProjectListModel(self.app.project_registry)) |
|
self.__filter_model.setSortRole(project_registry_lib.Project.NameRole) |
|
self.__filter_model.setSortCaseSensitivity(Qt.CaseInsensitive) |
|
self.__filter_model.sort(0, Qt.AscendingOrder) |
|
self.__filter_model.setFilterKeyColumn(0) |
|
self.__filter_model.setFilterRole(project_registry_lib.Project.NameRole) |
|
|
|
self.__search = QtWidgets.QLineEdit(self) |
|
search_action = QtWidgets.QAction(self.__search) |
|
search_action.setIcon(QtGui.QIcon.fromTheme('edit-find')) |
|
self.__search.addAction(search_action, QtWidgets.QLineEdit.LeadingPosition) |
|
clear_action = QtWidgets.QAction("Clear search string", self.__search) |
|
clear_action.setIcon(QtGui.QIcon.fromTheme('edit-clear')) |
|
clear_action.triggered.connect(self.__search.clear) |
|
self.__search.addAction(clear_action, QtWidgets.QLineEdit.TrailingPosition) |
|
self.__search.textChanged.connect(self.__filter_model.setFilterWords) |
|
self.__search.setText( |
|
self.app.settings.value('open-project-dialog/search-text', '')) |
|
self.__search.textChanged.connect( |
|
lambda text: self.app.settings.setValue('open-project-dialog/search-text', text)) |
|
|
|
self.__sort_mode = QtWidgets.QComboBox(self) |
|
self.__sort_mode.addItem("Name", 'name') |
|
self.__sort_mode.addItem("Last usage", 'mtime') |
|
self.__sort_mode.setCurrentIndex(self.__sort_mode.findData( |
|
self.app.settings.value('open-project-dialog/sort-mode', 'name'))) |
|
self.__sort_mode.currentIndexChanged.connect(self.__updateSort) |
|
|
|
self.__sort_dir = QtWidgets.QComboBox(self) |
|
self.__sort_dir.addItem("Ascending", 'asc') |
|
self.__sort_dir.addItem("Descending", 'desc') |
|
self.__sort_dir.setCurrentIndex(self.__sort_dir.findData( |
|
self.app.settings.value('open-project-dialog/sort-dir', 'asc'))) |
|
self.__sort_dir.currentIndexChanged.connect(self.__updateSort) |
|
|
|
self.__updateSort() |
|
|
|
self.__open_button = QtWidgets.QPushButton(self) |
|
self.__open_button.setObjectName('open') |
|
self.__open_button.setIcon(QtGui.QIcon.fromTheme('document-open')) |
|
self.__open_button.setText("Open") |
|
self.__open_button.clicked.connect(self.__openClicked) |
|
|
|
self.__new_project_button = QtWidgets.QPushButton(self) |
|
self.__new_project_button.setIcon(QtGui.QIcon.fromTheme('document-new')) |
|
self.__new_project_button.setText("New project") |
|
self.__new_project_button.clicked.connect(self.__newProjectClicked) |
|
|
|
self.__new_folder_button = QtWidgets.QPushButton(self) |
|
self.__new_folder_button.setIcon(QtGui.QIcon.fromTheme('folder-new')) |
|
self.__new_folder_button.setText("New folder") |
|
self.__new_folder_button.clicked.connect(self.__newFolderClicked) |
|
|
|
self.__delete_action = QtWidgets.QAction("Delete", self) |
|
self.__delete_action.setIcon(QtGui.QIcon.fromTheme('edit-delete')) |
|
self.__delete_action.triggered.connect(self.__deleteClicked) |
|
|
|
self.__debugger_action = QtWidgets.QAction("Open in debugger", self) |
|
self.__debugger_action.triggered.connect(self.__debuggerClicked) |
|
|
|
self.__more_menu = QtWidgets.QMenu() |
|
self.__more_menu.addAction(self.__delete_action) |
|
self.__more_menu.addAction(self.__debugger_action) |
|
|
|
self.__more_button = QtWidgets.QPushButton(self) |
|
self.__more_button.setText("More") |
|
self.__more_button.setMenu(self.__more_menu) |
|
|
|
self.__list = ProjectListView(self) |
|
self.__list.setObjectName('project-list') |
|
self.__list.setModel(self.__filter_model) |
|
self.__list.numProjectsSelected.connect(lambda _: self.__updateButtons()) |
|
self.__list.itemDoubleClicked.connect(self.__itemDoubleClicked) |
|
|
|
self.__updateButtons() |
|
self.app.project_registry.contentsChanged.connect(self.__updateButtons) |
|
|
|
l4 = QtWidgets.QHBoxLayout() |
|
l4.setContentsMargins(0, 0, 0, 0) |
|
l4.addWidget(self.__search) |
|
l4.addWidget(self.__sort_mode) |
|
l4.addWidget(self.__sort_dir) |
|
|
|
l3 = QtWidgets.QVBoxLayout() |
|
l3.setContentsMargins(0, 0, 0, 0) |
|
l3.addWidget(self.__open_button) |
|
l3.addWidget(self.__new_project_button) |
|
l3.addWidget(self.__new_folder_button) |
|
l3.addWidget(self.__more_button) |
|
l3.addStretch(1) |
|
|
|
l2 = QtWidgets.QHBoxLayout() |
|
l2.setContentsMargins(0, 0, 0, 0) |
|
l2.addWidget(self.__list) |
|
l2.addLayout(l3) |
|
|
|
l1 = QtWidgets.QVBoxLayout() |
|
l1.setContentsMargins(0, 0, 0, 0) |
|
l1.addLayout(l4) |
|
l1.addLayout(l2) |
|
self.setLayout(l1) |
|
|
|
def cleanup(self) -> None: |
|
pass |
|
|
|
def __updateButtons(self) -> None: |
|
selected_projects = self.__list.selectedProjects() |
|
|
|
self.__open_button.setEnabled( |
|
len(selected_projects) == 1 and not selected_projects[0].isOpened()) |
|
self.__delete_action.setEnabled( |
|
len(selected_projects) == 1 and not selected_projects[0].isOpened()) |
|
self.__debugger_action.setEnabled( |
|
len(selected_projects) == 1 and not selected_projects[0].isOpened()) |
|
|
|
def __updateSort(self) -> None: |
|
sort_mode = self.__sort_mode.currentData() |
|
if sort_mode == 'name': |
|
self.__filter_model.setSortRole(project_registry_lib.Project.NameRole) |
|
else: |
|
assert sort_mode == 'mtime' |
|
self.__filter_model.setSortRole(project_registry_lib.Project.MTimeRole) |
|
self.app.settings.setValue('open-project-dialog/sort-mode', sort_mode) |
|
|
|
sort_dir = self.__sort_dir.currentData() |
|
if sort_dir == 'asc': |
|
self.__filter_model.sort(0, Qt.AscendingOrder) |
|
else: |
|
assert sort_dir == 'desc' |
|
self.__filter_model.sort(0, Qt.DescendingOrder) |
|
self.app.settings.setValue('open-project-dialog/sort-dir', sort_dir) |
|
|
|
def __itemDoubleClicked(self, item: project_registry_lib.Item) -> None: |
|
if isinstance(item, project_registry_lib.Project) and not item.isOpened(): |
|
self.openProject(item) |
|
|
|
def __openClicked(self) -> None: |
|
selected_projects = self.__list.selectedProjects() |
|
if len(selected_projects) == 1: |
|
self.openProject(selected_projects[0]) |
|
|
|
def __newProjectClicked(self) -> None: |
|
dialog = NewProjectDialog(parent=self, context=self.context) |
|
dialog.setModal(True) |
|
dialog.finished.connect(functools.partial(self.__newProjectDialogDone, dialog)) |
|
dialog.show() |
|
|
|
def __newProjectDialogDone(self, dialog: NewProjectDialog, result: int) -> None: |
|
if result != QtWidgets.QDialog.Accepted: |
|
return |
|
|
|
self.createProject.emit(dialog.projectPath()) |
|
|
|
def __newFolderClicked(self) -> None: |
|
raise NotImplementedError |
|
|
|
def __deleteClicked(self) -> None: |
|
selected_projects = self.__list.selectedProjects() |
|
if len(selected_projects) == 1: |
|
project = selected_projects[0] |
|
|
|
dialog = QtWidgets.QMessageBox(self) |
|
dialog.setWindowTitle("noisicaä - Delete Project") |
|
dialog.setIcon(QtWidgets.QMessageBox.Warning) |
|
dialog.setText("Delete project \"%s\"?" % project.name) |
|
dialog.setInformativeText("All data will be irrevocably removed.") |
|
buttons = QtWidgets.QMessageBox.StandardButtons() |
|
buttons |= QtWidgets.QMessageBox.Ok |
|
buttons |= QtWidgets.QMessageBox.Cancel |
|
dialog.setStandardButtons(buttons) |
|
dialog.setModal(True) |
|
dialog.finished.connect( |
|
lambda result: self.call_async( |
|
self.__deleteDialogDone(project, result))) |
|
dialog.show() |
|
|
|
async def __deleteDialogDone( |
|
self, project: project_registry_lib.Project, result: int |
|
) -> None: |
|
if result != QtWidgets.QMessageBox.Ok: |
|
return |
|
|
|
self.setDisabled(True) |
|
await project.delete() |
|
await self.app.project_registry.refresh() |
|
self.setDisabled(False) |
|
|
|
def __debuggerClicked(self) -> None: |
|
selected_projects = self.__list.selectedProjects() |
|
if len(selected_projects) == 1: |
|
self.debugProject.emit(selected_projects[0]) |
|
|
|
def openProject(self, project: project_registry_lib.Project) -> None: |
|
self.projectSelected.emit(project)
|
|
|