diff --git a/.flake8 b/.flake8 index a2162126a..8c515f4d6 100644 --- a/.flake8 +++ b/.flake8 @@ -6,3 +6,5 @@ exclude = scratch_*.py, Bonsai, venv* + _version.py + ui_*.py diff --git a/.gitignore b/.gitignore index 6ea50b910..1de0a00cd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ app.log /.idea/* /venv/ *.autosave +iblrig/_version.py *.code-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..9ae347fd9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +Changelog +--------- + +------------------------------- + + +8.9.3 +----- +* re-implemented update notice + +8.9.2 +----- +* hot-fix for disabling the update-check - this will need work + +8.9.1 +----- +* hot-fix for missing live-plots + +8.9.0 +----- +* major rework of the GUI +* task-specific settings +* new dialogs for weight & droppings diff --git a/docs/source/conf.py b/docs/source/conf.py index 2d2d42ba6..0b0b49217 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,11 +1,11 @@ -from importlib.metadata import version from datetime import date +from iblrig import __version__ + project = 'iblrig' copyright = f'2018 – {date.today().year} International Brain Laboratory' author = 'International Brain Laboratory' -release = version('iblrig') -version = '.'.join(release.split('.')[:3]) +version = '.'.join(__version__.split('.')[:3]) # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/developer_guide.rst b/docs/source/developer_guide.rst index 94b86fb6b..4f87b1fef 100644 --- a/docs/source/developer_guide.rst +++ b/docs/source/developer_guide.rst @@ -15,20 +15,24 @@ Its version string (currently "|version|") is a combination of three fields, sep * The ``MINOR`` field will be incremented upon adding new, backwards compatible features. * The ``PATCH`` field will be incremented with each new, backwards compatible bugfix release that does not implement a new feature. -On the developer side, these 3 fields are manually controlled by adding the respective version string to a commit as a `git tag `_, for instance: +On the developer side, these 3 fields are manually controlled by, both -.. code-block:: console + 1. adjusting the variable ``__version__`` in ``iblrig/__init__.py``, and + 2. adding the corresponding version string to a commit as a `git tag `_, + for instance: + + .. code-block:: console - git tag 8.8.4 - git push origin --tags + git tag 8.8.4 + git push origin --tags -The version string displayed by IBLRIG *may* include additional fields, such as in "|version|.post3+dirty". +The version string displayed by IBLRIG *may* include additional fields, such as in "|version|-post3.dirty". Here, -* ``.post3`` indicates the third unversioned commit after the latest versioned release, and -* ``+dirty`` indicates the presence of uncommited changes in your local repository of IBLRIG. +* ``post3`` indicates the third unversioned commit after the latest versioned release, and +* ``dirty`` indicates the presence of uncommited changes in your local repository of IBLRIG. -Both of these fields are inferred by `setuptools_scm `_ and do not require manual interaction from the developer. +Both of these fields are inferred by means of git describe and do not require manual interaction from the developer. Running Tests Locally diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 0f89ee311..c63027ea3 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -20,10 +20,15 @@ To initiate a task through the graphical user interface, open a Windows PowerShe These commands activate the necessary environment and launch the IBL Rig Wizard GUI window, as shown below: -.. image:: gui.png - :alt: The IBLRIG GUI +.. figure:: gui.png + :alt: A screenshot of IBL Rig Wizard + :align: center -Complete the following actions within the GUI: + A screenshot of IBL Rig Wizard + + +Starting a Task +--------------- 1. Enter your Alyx username, then click on the *Connect* button. This action will automatically populate the GUI fields with information @@ -31,11 +36,13 @@ Complete the following actions within the GUI: 2. Select the desired values from the provided options. Utilize the *Filter* field to swiftly narrow down the list of displayed subjects. + Note that selections for *Project* and *Procedure* are mandatory. 3. Click the *Start* button to initiate the task. -Additionally, there are supplementary controls located in the “Flow” -section:: + +Supplementary Controls +---------------------- - If you check the *Append* option before clicking *Start*, the task you initiate will be linked to the preceding task, creating a diff --git a/iblrig/__init__.py b/iblrig/__init__.py index 751b1ba4d..47941e296 100644 --- a/iblrig/__init__.py +++ b/iblrig/__init__.py @@ -1,8 +1,9 @@ -from pathlib import Path -from setuptools_scm import get_version -from importlib.metadata import version +# PLEASE REMEMBER TO: +# 1) update CHANGELOG.md +# 2) git tag the release in accordance to the version number below (after merge!) +__version__ = '8.9.3' -if Path('.github').exists(): - __version__ = get_version(version_scheme='post-release', local_scheme='dirty-tag') -else: - __version__ = version('iblrig') +# The following method call will try to get post-release information (i.e. the number of commits since the last tagged +# release corresponding to the one above), plus information about the state of the local repository (dirty/broken) +from iblrig.version_management import get_detailed_version_string +__version__ = get_detailed_version_string(__version__) diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 25d89585d..e801b8d44 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -36,7 +36,6 @@ import iblrig.spacer import iblrig.alyx import iblrig.graphic as graph -from iblrig.version_management import check_for_updates import ibllib.io.session_params as ses_params from iblrig.transfer_experiments import BehaviorCopier @@ -75,26 +74,6 @@ def __init__(self, subject=None, task_parameter_file=None, file_hardware_setting self.logger = None self._setup_loggers(level=log_level) self.logger.info(f"Running iblrig {iblrig.__version__}, pybpod version {pybpodapi.__version__}") - - # check for update - if not wizard and not BaseSession.checked_for_update: - BaseSession.checked_for_update = True - update_status, remote_version = check_for_updates() - if update_status is True: - print(f"\nUpdate to iblrig {remote_version} is available!\n" - f"Please update by issuing:\n\n" - f" upgrade_iblrig\n") - while True: - print("- Press [Enter] to exit IBL Rig and perform the update right away.\n" - "- Enter 'I will update later' to continue without updating.") - response = input('Your response: ') - if response == '': - print("\nEnter 'git pull' - then restart iblrig. Thanks for keeping iblrig up to date!") - exit() - elif response == 'I will update later': - print("\nPlease do so!") - break - self.interactive = False if append else interactive self._one = one self.init_datetime = datetime.datetime.now() diff --git a/iblrig/constants.py b/iblrig/constants.py new file mode 100644 index 000000000..2da3d9984 --- /dev/null +++ b/iblrig/constants.py @@ -0,0 +1,5 @@ +from pathlib import Path +from shutil import which + +BASE_DIR = str(Path(__file__).parents[1]) +IS_GIT = Path(BASE_DIR).joinpath('.git').exists() and which('git') is not None diff --git a/iblrig/gui/ui_update.py b/iblrig/gui/ui_update.py new file mode 100644 index 000000000..c65e6cdcd --- /dev/null +++ b/iblrig/gui/ui_update.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'iblrig/gui/ui_update.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_update(object): + def setupUi(self, update): + update.setObjectName("update") + update.resize(353, 496) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(update.sizePolicy().hasHeightForWidth()) + update.setSizePolicy(sizePolicy) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/images/wizard.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + update.setWindowIcon(icon) + update.setModal(True) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(update) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.uiLayoutLogo = QtWidgets.QVBoxLayout() + self.uiLayoutLogo.setContentsMargins(-1, -1, 6, -1) + self.uiLayoutLogo.setObjectName("uiLayoutLogo") + self.uiLabelLogo = QtWidgets.QLabel(update) + self.uiLabelLogo.setMaximumSize(QtCore.QSize(64, 64)) + self.uiLabelLogo.setText("") + self.uiLabelLogo.setPixmap(QtGui.QPixmap("iblrig/gui\\wizard.png")) + self.uiLabelLogo.setScaledContents(True) + self.uiLabelLogo.setObjectName("uiLabelLogo") + self.uiLayoutLogo.addWidget(self.uiLabelLogo) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.uiLayoutLogo.addItem(spacerItem) + self.horizontalLayout_2.addLayout(self.uiLayoutLogo) + self.uiLayoutRight = QtWidgets.QVBoxLayout() + self.uiLayoutRight.setObjectName("uiLayoutRight") + self.uiLabelHeader = QtWidgets.QLabel(update) + self.uiLabelHeader.setObjectName("uiLabelHeader") + self.uiLayoutRight.addWidget(self.uiLabelHeader) + self.uiTextBrowserChanges = QtWidgets.QTextBrowser(update) + self.uiTextBrowserChanges.setStyleSheet("") + self.uiTextBrowserChanges.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.uiTextBrowserChanges.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.uiTextBrowserChanges.setDocumentTitle("") + self.uiTextBrowserChanges.setMarkdown("") + self.uiTextBrowserChanges.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.uiTextBrowserChanges.setObjectName("uiTextBrowserChanges") + self.uiLayoutRight.addWidget(self.uiTextBrowserChanges) + self.uiLabelFooter = QtWidgets.QLabel(update) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiLabelFooter.sizePolicy().hasHeightForWidth()) + self.uiLabelFooter.setSizePolicy(sizePolicy) + self.uiLabelFooter.setAlignment(QtCore.Qt.AlignJustify|QtCore.Qt.AlignVCenter) + self.uiLabelFooter.setWordWrap(True) + self.uiLabelFooter.setObjectName("uiLabelFooter") + self.uiLayoutRight.addWidget(self.uiLabelFooter) + spacerItem1 = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + self.uiLayoutRight.addItem(spacerItem1) + self.uiLayoutCommand = QtWidgets.QHBoxLayout() + self.uiLayoutCommand.setObjectName("uiLayoutCommand") + spacerItem2 = QtWidgets.QSpacerItem(1, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.uiLayoutCommand.addItem(spacerItem2) + self.uiLineEditCommand = QtWidgets.QLineEdit(update) + self.uiLineEditCommand.setAlignment(QtCore.Qt.AlignCenter) + self.uiLineEditCommand.setReadOnly(True) + self.uiLineEditCommand.setObjectName("uiLineEditCommand") + self.uiLayoutCommand.addWidget(self.uiLineEditCommand) + spacerItem3 = QtWidgets.QSpacerItem(1, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.uiLayoutCommand.addItem(spacerItem3) + self.uiLayoutRight.addLayout(self.uiLayoutCommand) + spacerItem4 = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + self.uiLayoutRight.addItem(spacerItem4) + self.uiLine = QtWidgets.QFrame(update) + self.uiLine.setFrameShape(QtWidgets.QFrame.HLine) + self.uiLine.setFrameShadow(QtWidgets.QFrame.Sunken) + self.uiLine.setObjectName("uiLine") + self.uiLayoutRight.addWidget(self.uiLine) + self.uiLayoutButton = QtWidgets.QHBoxLayout() + self.uiLayoutButton.setObjectName("uiLayoutButton") + spacerItem5 = QtWidgets.QSpacerItem(1, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.uiLayoutButton.addItem(spacerItem5) + self.uiPushButtonOK = QtWidgets.QPushButton(update) + self.uiPushButtonOK.setObjectName("uiPushButtonOK") + self.uiLayoutButton.addWidget(self.uiPushButtonOK) + self.uiLayoutRight.addLayout(self.uiLayoutButton) + self.horizontalLayout_2.addLayout(self.uiLayoutRight) + self.horizontalLayout_2.setStretch(1, 100) + + self.retranslateUi(update) + QtCore.QMetaObject.connectSlotsByName(update) + + def retranslateUi(self, update): + _translate = QtCore.QCoreApplication.translate + update.setWindowTitle(_translate("update", "Update Notice")) + self.uiLabelHeader.setText(_translate("update", "Update Available!")) + self.uiLabelFooter.setText(_translate("update", "To update, close IBL Rig Wizard and run the following command within the iblrigv8 Python environment:")) + self.uiLineEditCommand.setText(_translate("update", "iblrig_update")) + self.uiPushButtonOK.setText(_translate("update", "OK")) diff --git a/iblrig/gui/ui_update.ui b/iblrig/gui/ui_update.ui new file mode 100644 index 000000000..ff3604db1 --- /dev/null +++ b/iblrig/gui/ui_update.ui @@ -0,0 +1,231 @@ + + + update + + + + 0 + 0 + 353 + 496 + + + + + 0 + 0 + + + + Update Notice + + + + :/images/wizard.png:/images/wizard.png + + + true + + + + + + 6 + + + + + + 64 + 64 + + + + + + + wizard.png + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Update Available! + + + + + + + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + + + + + + + Qt::NoTextInteraction + + + + + + + + 0 + 0 + + + + To update, close IBL Rig Wizard and run the following command within the iblrigv8 Python environment: + + + Qt::AlignJustify|Qt::AlignVCenter + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + Qt::Horizontal + + + + 1 + 20 + + + + + + + + iblrig_update + + + Qt::AlignCenter + + + true + + + + + + + Qt::Horizontal + + + + 1 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + 1 + 20 + + + + + + + + OK + + + + + + + + + + + + diff --git a/iblrig/gui/ui_wizard.py b/iblrig/gui/ui_wizard.py new file mode 100644 index 000000000..c570b1dcd --- /dev/null +++ b/iblrig/gui/ui_wizard.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'iblrig/gui/ui_wizard.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_wizard(object): + def setupUi(self, wizard): + wizard.setObjectName("wizard") + wizard.resize(350, 527) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(wizard.sizePolicy().hasHeightForWidth()) + wizard.setSizePolicy(sizePolicy) + wizard.setMinimumSize(QtCore.QSize(0, 0)) + wizard.setMaximumSize(QtCore.QSize(16777215, 16777215)) + wizard.setSizeIncrement(QtCore.QSize(0, 0)) + wizard.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap("iblrig/gui\\wizard.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + wizard.setWindowIcon(icon) + wizard.setWindowOpacity(1.0) + wizard.setAutoFillBackground(False) + wizard.setAnimated(False) + wizard.setDocumentMode(False) + self.centralwidget = QtWidgets.QWidget(wizard) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) + self.centralwidget.setSizePolicy(sizePolicy) + self.centralwidget.setMinimumSize(QtCore.QSize(0, 0)) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout_3.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_3.setSpacing(6) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.mainGrid = QtWidgets.QGridLayout() + self.mainGrid.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.mainGrid.setContentsMargins(6, 0, 6, 0) + self.mainGrid.setObjectName("mainGrid") + self.uiGroupParameters = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiGroupParameters.sizePolicy().hasHeightForWidth()) + self.uiGroupParameters.setSizePolicy(sizePolicy) + self.uiGroupParameters.setObjectName("uiGroupParameters") + self.formLayout = QtWidgets.QFormLayout(self.uiGroupParameters) + self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) + self.formLayout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.formLayout.setObjectName("formLayout") + self.label = QtWidgets.QLabel(self.uiGroupParameters) + self.label.setObjectName("label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) + self.frame_3 = QtWidgets.QFrame(self.uiGroupParameters) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth()) + self.frame_3.setSizePolicy(sizePolicy) + self.frame_3.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_3.setFrameShadow(QtWidgets.QFrame.Plain) + self.frame_3.setObjectName("frame_3") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.frame_3) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.uiComboUser = QtWidgets.QComboBox(self.frame_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(3) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiComboUser.sizePolicy().hasHeightForWidth()) + self.uiComboUser.setSizePolicy(sizePolicy) + self.uiComboUser.setEditable(True) + self.uiComboUser.setObjectName("uiComboUser") + self.horizontalLayout_2.addWidget(self.uiComboUser) + self.uiPushConnect = QtWidgets.QPushButton(self.frame_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(2) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiPushConnect.sizePolicy().hasHeightForWidth()) + self.uiPushConnect.setSizePolicy(sizePolicy) + self.uiPushConnect.setMaximumSize(QtCore.QSize(150, 16777215)) + self.uiPushConnect.setToolTip("") + self.uiPushConnect.setObjectName("uiPushConnect") + self.horizontalLayout_2.addWidget(self.uiPushConnect) + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.frame_3) + self.label_2 = QtWidgets.QLabel(self.uiGroupParameters) + self.label_2.setObjectName("label_2") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) + self.frame_2 = QtWidgets.QFrame(self.uiGroupParameters) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) + self.frame_2.setSizePolicy(sizePolicy) + self.frame_2.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Plain) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.uiComboSubject = QtWidgets.QComboBox(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(3) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiComboSubject.sizePolicy().hasHeightForWidth()) + self.uiComboSubject.setSizePolicy(sizePolicy) + self.uiComboSubject.setMinimumSize(QtCore.QSize(0, 0)) + self.uiComboSubject.setObjectName("uiComboSubject") + self.horizontalLayout.addWidget(self.uiComboSubject) + self.lineEditSubject = QtWidgets.QLineEdit(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(2) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditSubject.sizePolicy().hasHeightForWidth()) + self.lineEditSubject.setSizePolicy(sizePolicy) + self.lineEditSubject.setLayoutDirection(QtCore.Qt.LeftToRight) + self.lineEditSubject.setObjectName("lineEditSubject") + self.horizontalLayout.addWidget(self.lineEditSubject) + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.frame_2) + self.label_4 = QtWidgets.QLabel(self.uiGroupParameters) + self.label_4.setObjectName("label_4") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_4) + self.uiComboTask = QtWidgets.QComboBox(self.uiGroupParameters) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiComboTask.sizePolicy().hasHeightForWidth()) + self.uiComboTask.setSizePolicy(sizePolicy) + self.uiComboTask.setMinimumSize(QtCore.QSize(0, 0)) + self.uiComboTask.setObjectName("uiComboTask") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.uiComboTask) + self.label_3 = QtWidgets.QLabel(self.uiGroupParameters) + self.label_3.setObjectName("label_3") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_3) + self.uiListProjects = QtWidgets.QListView(self.uiGroupParameters) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiListProjects.sizePolicy().hasHeightForWidth()) + self.uiListProjects.setSizePolicy(sizePolicy) + self.uiListProjects.setMaximumSize(QtCore.QSize(16777215, 80)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, brush) + self.uiListProjects.setPalette(palette) + self.uiListProjects.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.uiListProjects.setFocusPolicy(QtCore.Qt.TabFocus) + self.uiListProjects.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.uiListProjects.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + self.uiListProjects.setObjectName("uiListProjects") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.uiListProjects) + self.label_5 = QtWidgets.QLabel(self.uiGroupParameters) + self.label_5.setObjectName("label_5") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_5) + self.uiListProcedures = QtWidgets.QListView(self.uiGroupParameters) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiListProcedures.sizePolicy().hasHeightForWidth()) + self.uiListProcedures.setSizePolicy(sizePolicy) + self.uiListProcedures.setMaximumSize(QtCore.QSize(16777215, 80)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, brush) + self.uiListProcedures.setPalette(palette) + self.uiListProcedures.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.uiListProcedures.setFocusPolicy(QtCore.Qt.TabFocus) + self.uiListProcedures.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.uiListProcedures.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + self.uiListProcedures.setObjectName("uiListProcedures") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.uiListProcedures) + self.mainGrid.addWidget(self.uiGroupParameters, 1, 0, 1, 2) + self.uiGroupTaskParameters = QtWidgets.QGroupBox(self.centralwidget) + self.uiGroupTaskParameters.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiGroupTaskParameters.sizePolicy().hasHeightForWidth()) + self.uiGroupTaskParameters.setSizePolicy(sizePolicy) + self.uiGroupTaskParameters.setMinimumSize(QtCore.QSize(0, 0)) + self.uiGroupTaskParameters.setObjectName("uiGroupTaskParameters") + self.formLayout_3 = QtWidgets.QFormLayout(self.uiGroupTaskParameters) + self.formLayout_3.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.formLayout_3.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.formLayout_3.setObjectName("formLayout_3") + self.mainGrid.addWidget(self.uiGroupTaskParameters, 2, 0, 1, 2) + self.uiGroupDiskSpace = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiGroupDiskSpace.sizePolicy().hasHeightForWidth()) + self.uiGroupDiskSpace.setSizePolicy(sizePolicy) + self.uiGroupDiskSpace.setObjectName("uiGroupDiskSpace") + self.formLayout_2 = QtWidgets.QFormLayout(self.uiGroupDiskSpace) + self.formLayout_2.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.formLayout_2.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.formLayout_2.setSpacing(0) + self.formLayout_2.setObjectName("formLayout_2") + self.uiProgressDiskSpace = QtWidgets.QProgressBar(self.uiGroupDiskSpace) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiProgressDiskSpace.sizePolicy().hasHeightForWidth()) + self.uiProgressDiskSpace.setSizePolicy(sizePolicy) + self.uiProgressDiskSpace.setStatusTip("") + self.uiProgressDiskSpace.setProperty("value", 24) + self.uiProgressDiskSpace.setInvertedAppearance(False) + self.uiProgressDiskSpace.setTextDirection(QtWidgets.QProgressBar.TopToBottom) + self.uiProgressDiskSpace.setObjectName("uiProgressDiskSpace") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.uiProgressDiskSpace) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setSpacing(0) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.formLayout_4 = QtWidgets.QFormLayout() + self.formLayout_4.setContentsMargins(-1, 6, -1, -1) + self.formLayout_4.setVerticalSpacing(6) + self.formLayout_4.setObjectName("formLayout_4") + self.uiLableDiskAvailable = QtWidgets.QLabel(self.uiGroupDiskSpace) + self.uiLableDiskAvailable.setObjectName("uiLableDiskAvailable") + self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.uiLableDiskAvailable) + self.uiLableDiskAvailableValue = QtWidgets.QLabel(self.uiGroupDiskSpace) + self.uiLableDiskAvailableValue.setTextFormat(QtCore.Qt.AutoText) + self.uiLableDiskAvailableValue.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.uiLableDiskAvailableValue.setObjectName("uiLableDiskAvailableValue") + self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.uiLableDiskAvailableValue) + self.uiLableDiskIblrig = QtWidgets.QLabel(self.uiGroupDiskSpace) + self.uiLableDiskIblrig.setObjectName("uiLableDiskIblrig") + self.formLayout_4.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.uiLableDiskIblrig) + self.uiLableDiskIblrigValue = QtWidgets.QLabel(self.uiGroupDiskSpace) + self.uiLableDiskIblrigValue.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.uiLableDiskIblrigValue.setObjectName("uiLableDiskIblrigValue") + self.formLayout_4.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.uiLableDiskIblrigValue) + self.horizontalLayout_4.addLayout(self.formLayout_4) + self.formLayout_2.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_4) + self.mainGrid.addWidget(self.uiGroupDiskSpace, 4, 1, 1, 1) + self.uiGroupTools = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiGroupTools.sizePolicy().hasHeightForWidth()) + self.uiGroupTools.setSizePolicy(sizePolicy) + self.uiGroupTools.setObjectName("uiGroupTools") + self.gridLayout_3 = QtWidgets.QGridLayout(self.uiGroupTools) + self.gridLayout_3.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.gridLayout_3.setObjectName("gridLayout_3") + self.uiPushFlush = QtWidgets.QPushButton(self.uiGroupTools) + self.uiPushFlush.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiPushFlush.sizePolicy().hasHeightForWidth()) + self.uiPushFlush.setSizePolicy(sizePolicy) + self.uiPushFlush.setCheckable(True) + self.uiPushFlush.setObjectName("uiPushFlush") + self.gridLayout_3.addWidget(self.uiPushFlush, 0, 0, 1, 1) + self.uiPushHelp = QtWidgets.QPushButton(self.uiGroupTools) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiPushHelp.sizePolicy().hasHeightForWidth()) + self.uiPushHelp.setSizePolicy(sizePolicy) + self.uiPushHelp.setObjectName("uiPushHelp") + self.gridLayout_3.addWidget(self.uiPushHelp, 1, 0, 1, 1) + self.mainGrid.addWidget(self.uiGroupTools, 4, 0, 1, 1) + self.uiGroupSessionControl = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiGroupSessionControl.sizePolicy().hasHeightForWidth()) + self.uiGroupSessionControl.setSizePolicy(sizePolicy) + self.uiGroupSessionControl.setObjectName("uiGroupSessionControl") + self.gridLayout = QtWidgets.QGridLayout(self.uiGroupSessionControl) + self.gridLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.gridLayout.setContentsMargins(9, -1, -1, -1) + self.gridLayout.setHorizontalSpacing(6) + self.gridLayout.setObjectName("gridLayout") + self.uiPushPause = QtWidgets.QPushButton(self.uiGroupSessionControl) + self.uiPushPause.setEnabled(False) + self.uiPushPause.setCheckable(True) + self.uiPushPause.setChecked(False) + self.uiPushPause.setObjectName("uiPushPause") + self.gridLayout.addWidget(self.uiPushPause, 2, 1, 1, 1) + self.uiPushStart = QtWidgets.QPushButton(self.uiGroupSessionControl) + self.uiPushStart.setStyleSheet("QPushButton { background-color: red; }") + self.uiPushStart.setObjectName("uiPushStart") + self.gridLayout.addWidget(self.uiPushStart, 2, 2, 1, 1) + self.uiCheckAppend = QtWidgets.QCheckBox(self.uiGroupSessionControl) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uiCheckAppend.sizePolicy().hasHeightForWidth()) + self.uiCheckAppend.setSizePolicy(sizePolicy) + self.uiCheckAppend.setToolTip("") + self.uiCheckAppend.setObjectName("uiCheckAppend") + self.gridLayout.addWidget(self.uiCheckAppend, 3, 2, 1, 1) + self.mainGrid.addWidget(self.uiGroupSessionControl, 3, 0, 1, 2) + self.horizontalLayout_3.addLayout(self.mainGrid) + wizard.setCentralWidget(self.centralwidget) + self.statusbar = QtWidgets.QStatusBar(wizard) + self.statusbar.setEnabled(True) + self.statusbar.setToolTip("") + self.statusbar.setSizeGripEnabled(False) + self.statusbar.setObjectName("statusbar") + wizard.setStatusBar(self.statusbar) + self.uiActionLoadSession = QtWidgets.QAction(wizard) + self.uiActionLoadSession.setObjectName("uiActionLoadSession") + self.uiActionRecent = QtWidgets.QAction(wizard) + self.uiActionRecent.setObjectName("uiActionRecent") + self.uiActionTrainingTemplate = QtWidgets.QAction(wizard) + self.uiActionTrainingTemplate.setObjectName("uiActionTrainingTemplate") + self.uiActionEphysTemplate = QtWidgets.QAction(wizard) + self.uiActionEphysTemplate.setObjectName("uiActionEphysTemplate") + + self.retranslateUi(wizard) + QtCore.QMetaObject.connectSlotsByName(wizard) + wizard.setTabOrder(self.uiComboUser, self.uiPushConnect) + wizard.setTabOrder(self.uiPushConnect, self.uiComboSubject) + wizard.setTabOrder(self.uiComboSubject, self.lineEditSubject) + wizard.setTabOrder(self.lineEditSubject, self.uiComboTask) + wizard.setTabOrder(self.uiComboTask, self.uiListProjects) + wizard.setTabOrder(self.uiListProjects, self.uiListProcedures) + wizard.setTabOrder(self.uiListProcedures, self.uiPushPause) + wizard.setTabOrder(self.uiPushPause, self.uiPushStart) + wizard.setTabOrder(self.uiPushStart, self.uiCheckAppend) + wizard.setTabOrder(self.uiCheckAppend, self.uiPushFlush) + wizard.setTabOrder(self.uiPushFlush, self.uiPushHelp) + + def retranslateUi(self, wizard): + _translate = QtCore.QCoreApplication.translate + wizard.setWindowTitle(_translate("wizard", "IBL Rig Wizard")) + self.uiGroupParameters.setTitle(_translate("wizard", "General Parameters")) + self.label.setText(_translate("wizard", "Alyx User")) + self.uiComboUser.setStatusTip(_translate("wizard", "enter or select your Alyx username")) + self.uiPushConnect.setStatusTip(_translate("wizard", "connect to Alyx")) + self.uiPushConnect.setText(_translate("wizard", "Connect")) + self.label_2.setText(_translate("wizard", "Subject")) + self.uiComboSubject.setStatusTip(_translate("wizard", "choose a subject")) + self.lineEditSubject.setStatusTip(_translate("wizard", "filter displayed subjects by name")) + self.lineEditSubject.setPlaceholderText(_translate("wizard", "Filter")) + self.label_4.setText(_translate("wizard", "Task")) + self.uiComboTask.setStatusTip(_translate("wizard", "choose a task for the session")) + self.label_3.setText(_translate("wizard", "Project")) + self.uiListProjects.setStatusTip(_translate("wizard", "select one or several projects for the session (mandatory)")) + self.label_5.setText(_translate("wizard", "Procedure")) + self.uiListProcedures.setStatusTip(_translate("wizard", "select one or several procedures for the session (mandatory)")) + self.uiGroupTaskParameters.setTitle(_translate("wizard", "Task Specific Parameters")) + self.uiGroupDiskSpace.setTitle(_translate("wizard", "Disk Usage")) + self.uiLableDiskAvailable.setText(_translate("wizard", "Available Space:")) + self.uiLableDiskAvailableValue.setText(_translate("wizard", "80.3 GB")) + self.uiLableDiskIblrig.setText(_translate("wizard", "IBL Rig Data:")) + self.uiLableDiskIblrigValue.setText(_translate("wizard", "1.2 GB")) + self.uiGroupTools.setTitle(_translate("wizard", "Tools")) + self.uiPushFlush.setStatusTip(_translate("wizard", "flush the valve")) + self.uiPushFlush.setText(_translate("wizard", "Flush")) + self.uiPushHelp.setStatusTip(_translate("wizard", "open the iblrig documentation in your browser")) + self.uiPushHelp.setText(_translate("wizard", "Help!")) + self.uiGroupSessionControl.setTitle(_translate("wizard", "Session Control")) + self.uiPushPause.setStatusTip(_translate("wizard", "pause the session after the current trial")) + self.uiPushPause.setText(_translate("wizard", "Pause")) + self.uiPushStart.setStatusTip(_translate("wizard", "start the session")) + self.uiPushStart.setText(_translate("wizard", "Start")) + self.uiCheckAppend.setStatusTip(_translate("wizard", "append to previous session")) + self.uiCheckAppend.setText(_translate("wizard", "Append")) + self.uiActionLoadSession.setText(_translate("wizard", "load session")) + self.uiActionRecent.setText(_translate("wizard", "recent")) + self.uiActionTrainingTemplate.setText(_translate("wizard", "training rig")) + self.uiActionEphysTemplate.setText(_translate("wizard", "ephys rig")) diff --git a/iblrig/gui/wizard.ui b/iblrig/gui/ui_wizard.ui similarity index 99% rename from iblrig/gui/wizard.ui rename to iblrig/gui/ui_wizard.ui index 65da20fce..7c434757f 100644 --- a/iblrig/gui/wizard.ui +++ b/iblrig/gui/ui_wizard.ui @@ -1,7 +1,7 @@ - ioWindowRigWizard - + wizard + 0 diff --git a/iblrig/gui/wizard.py b/iblrig/gui/wizard.py index 3ecc08f8d..a71865a99 100644 --- a/iblrig/gui/wizard.py +++ b/iblrig/gui/wizard.py @@ -10,19 +10,23 @@ import yaml import traceback import webbrowser -from random import choice +import ctypes +import os -from PyQt5 import QtWidgets, QtCore, uic +from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtWidgets import QStyle from one.api import ONE import iblrig_tasks import iblrig_custom_tasks import iblrig.path_helper +from iblrig.constants import BASE_DIR from iblrig.misc import _get_task_argument_parser from iblrig.base_tasks import BaseSession from iblrig.hardware import Bpod from iblrig.version_management import check_for_updates +from iblrig.gui.ui_wizard import Ui_wizard +from iblrig.gui.ui_update import Ui_update from pybpodapi import exceptions PROCEDURES = [ @@ -39,6 +43,8 @@ 'practice' ] +WIZARD_PNG = str(Path(BASE_DIR).joinpath('iblrig', 'gui', 'wizard.png')) + # this class gets called to get the path constructor utility to predict the session path class EmptySession(BaseSession): @@ -125,10 +131,12 @@ def connect(self, username=None, one=None): self.all_projects = sorted(set(projects + self.all_projects)) -class RigWizard(QtWidgets.QMainWindow): +class RigWizard(QtWidgets.QMainWindow, Ui_wizard): def __init__(self, *args, **kwargs): super(RigWizard, self).__init__(*args, **kwargs) - uic.loadUi(Path(__file__).parent.joinpath('wizard.ui'), self) + self.setupUi(self) + self.setWindowIcon(QtGui.QIcon(WIZARD_PNG)) + self.settings = QtCore.QSettings('iblrig', 'wizard') self.model = RigWizardModel() self.model2view() @@ -164,17 +172,17 @@ def __init__(self, *args, **kwargs): total_space, total_used, total_free = shutil.disk_usage(local_data.anchor) self.uiProgressDiskSpace.setStatusTip(f'utilization of drive {local_data.anchor}') self.uiProgressDiskSpace.setValue(round(total_used / total_space * 100)) - self.uiLableDiskAvailableValue.setText(f'{total_free / 1024**3 : .1f} GB') - self.uiLableDiskIblrigValue.setText(f'{v8data_size / 1024**3 : .1f} GB') + self.uiLableDiskAvailableValue.setText(f'{total_free / 1024 ** 3 : .1f} GB') + self.uiLableDiskIblrigValue.setText(f'{v8data_size / 1024 ** 3 : .1f} GB') tmp = QtWidgets.QLabel(f'iblrig v{iblrig.__version__}') tmp.setContentsMargins(4, 0, 0, 0) self.statusbar.addWidget(tmp) self.controls_for_extra_parameters() - self.setDisabled(True) - QtCore.QTimer.singleShot(1, self.check_dirty) - QtCore.QTimer.singleShot(1, self.check_for_update) + self.setDisabled(True) + QtCore.QTimer.singleShot(100, self.check_dirty) + QtCore.QTimer.singleShot(100, self.check_for_update) def closeEvent(self, event): if self.running_task_process is None: @@ -195,6 +203,19 @@ def closeEvent(self, event): event.accept() def check_dirty(self): + """ + Check if the iblrig installation contains local changes. + + This method checks if the installed version of iblrig contains local changes + (indicated by the version string ending with 'dirty'). If local changes are + detected, it displays a warning message to inform the user about potential + issues and provides instructions on how to reset the repository to its + default state. + + Returns + ------- + None + """ if not iblrig.__version__.endswith('dirty'): return msg_box = QtWidgets.QMessageBox(parent=self) @@ -204,29 +225,16 @@ def check_dirty(self): msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) msg_box.setIcon(QtWidgets.QMessageBox().Information) msg_box.exec_() - self.setDisabled(False) def check_for_update(self): - self.setDisabled(False) - return - self.statusbar.showMessage("Checking for updates ...") update_available, remote_version = check_for_updates() - if update_available: - cmdBox = QtWidgets.QLineEdit('upgrade_iblrig') - cmdBox.setReadOnly(True) - msgBox = QtWidgets.QMessageBox(parent=self) - msgBox.setWindowTitle("Update Notice") - msgBox.setText(f"Update to iblrig {remote_version} is available.\n\n" - f"Please update iblrig by issuing:") - msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok) - msgBox.setIcon(QtWidgets.QMessageBox().Information) - msgBox.layout().addWidget(cmdBox, 1, 2) - msgBox.findChild(QtWidgets.QPushButton).setText( - choice(['Yes, I promise!', 'I will do so immediately!', - 'Straight away!', 'Of course I will!'])) - msgBox.exec_() + self.setDisabled(False) - self.statusbar.clearMessage() + if update_available: + dialog = UpdateNotice(parent=self) + dialog.uiLabelHeader.setText(f"Update to iblrig {remote_version} is available.") + dialog.uiPushButtonOK.released.connect(lambda: dialog.close()) + dialog.exec_() def model2view(self): # stores the current values in the model @@ -496,7 +504,23 @@ def enable_UI_elements(self): self.repaint() +class UpdateNotice(QtWidgets.QDialog, Ui_update): + def __init__(self, parent=None, *args, **kwargs): + super(UpdateNotice, self).__init__(*args, **kwargs) + self.setupUi(self) + with open(Path(BASE_DIR).joinpath('CHANGELOG.md')) as f: + changelog = f.read() + self.uiTextBrowserChanges.setMarkdown(changelog) + self.uiTextBrowserChanges.setHtml(self.uiTextBrowserChanges.toHtml()) + self.uiLabelLogo.setPixmap(QtGui.QPixmap(WIZARD_PNG)) + self.setWindowIcon(QtGui.QIcon(WIZARD_PNG)) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) + + def main(): + if os.name == 'nt': + appid = f'IBL.iblrig.wizard.{iblrig.__version__}' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) app = QtWidgets.QApplication([]) app.setStyle("Fusion") w = RigWizard() diff --git a/iblrig/version_management.py b/iblrig/version_management.py index 74ca49a4b..cf490838f 100644 --- a/iblrig/version_management.py +++ b/iblrig/version_management.py @@ -1,108 +1,297 @@ +from typing import Union, Callable, Any + from packaging import version -from pathlib import Path -from re import sub -from subprocess import check_output, check_call, SubprocessError +import re +from subprocess import check_output, check_call, SubprocessError, CalledProcessError, STDOUT import sys -import iblrig +from iblrig import __version__ +from iblrig.constants import BASE_DIR, IS_GIT from iblutil.util import setup_logger log = setup_logger('iblrig') -def check_for_updates(): +def static_vars(**kwargs) -> Callable[..., Any]: + """ + Decorator to add static variables to a function. + + This decorator allows you to add static variables to a function by providing + keyword arguments. Static variables are shared across all calls to the + decorated function. + + Parameters + ---------- + **kwargs + Keyword arguments where the keys are variable names and the values are + the initial values of the static variables. + + Returns + ------- + function + A decorated function with the specified static variables. + """ + def decorate(func: Callable[..., Any]) -> Callable[..., Any]: + for k in kwargs: + setattr(func, k, kwargs[k]) + return func + return decorate + + +def check_for_updates() -> tuple[bool, Union[str, None]]: + """ + Check for updates to the iblrig software. - return False, iblrig.__version__ + This function compares the locally installed version of iblrig with the + latest available version to determine if an update is available. + Returns: + tuple[bool, Union[str, None]]: A tuple containing two elements. + - A boolean indicating whether an update is available. + - A string representing the latest available version, or None if + no remote version information is available. + """ log.info('Checking for updates ...') + update_available = False + v_local = get_local_version() + v_remote = get_remote_version() + + if all((v_remote, v_local)): + v_remote_base = version.parse(v_remote.base_version) + v_local_base = version.parse(v_local.base_version) + + if v_remote_base > v_local_base: + log.info(f'Update to iblrig {v_remote.base_version} available.') + else: + log.info('No update available.') + update_available = v_remote > v_local + + return update_available, v_remote.base_version if v_remote else '' + + +def get_local_version() -> Union[version.Version, None]: + """ + Parse the local version string to obtain a Version object. + + This function attempts to parse the local version string (__version__) + and returns a Version object representing the parsed version. If the + parsing fails, it logs an error and returns None. + + Returns + ------- + Union[version.Version, None] + A Version object representing the parsed local version, or None if + parsing fails. + """ try: - v_local = version.parse(iblrig.__version__) + log.debug('Parsing local version string') + return version.parse(__version__) except (version.InvalidVersion, TypeError): - log.debug('Could not parse local version string') - return -1, '' + log.error(f'Could not parse local version string: {__version__}') + return None + - v_remote = Remote().version() - if v_remote is None: - log.debug('Could not parse remote version string') - return -1, '' +def get_detailed_version_string(v_basic: str) -> str: + """ + Generate a detailed version string based on a basic version string. - if v_remote > v_local: - log.info(f'Update to iblrig {v_remote} found.') - else: - log.info('No update found.') - return v_remote > v_local, str(v_remote) + This function takes a basic version string (major.minor.patch) and generates + a detailed version string by querying Git for additional version information. + The detailed version includes commit number of commits since the last tag, + and Git status (dirty or broken). It is designed to fail safely. + Parameters + ---------- + v_basic : str + A basic version string in the format 'major.minor.patch'. -def update_available(): - version_local = version.parse(iblrig.__version__) - version_remote = version.parse(Remote().version_str()) - return version_remote > version_local + Returns + ------- + str + A detailed version string containing version information retrieved + from Git, or the original basic version string if Git information + cannot be obtained. + Notes + ----- + This method will only work with installations managed through Git. + """ -class Remote(object): - _version = None + # this method will only work with installations managed through git + if not IS_GIT: + log.error('This installation of IBLRIG is not managed through git.') + return v_basic - @staticmethod - def version(): + # sanitize & check if input only consists of three fields - major, minor and patch - separated by dots + v_sanitized = re.sub(r'^(\d+\.\d+\.\d+).*$$', r'\1', v_basic) + if not re.match(r'^\d+\.\d+\.\d+$', v_sanitized): + log.error(f'Couldn\'t parse version string: {v_basic}') + return v_basic - if Remote._version: - return Remote._version + # get details through `git describe` + try: + fetch_remote_tags() + v_detailed = check_output(["git", "describe", "--dirty", "--broken", "--match", v_sanitized, "--tags", "--long"], + cwd=BASE_DIR, text=True, timeout=1, stderr=STDOUT) + except (SubprocessError, CalledProcessError): + log.error('Error calling `git describe`') + return v_basic + + # apply a bit of regex magic for formatting & return the detailed version string + v_detailed = re.sub(r'^((?:[\d+\.])+)(-\d+)?(-\w+)(-dirty|-broken)?\n$', r'\1\2\4', v_detailed) + v_detailed = re.sub(r'-(\d+)', r'-post\1', v_detailed) + v_detailed = re.sub(r'\-(dirty|broken)', r'.\1', v_detailed) + return v_detailed - if not is_git(): - return None - try: - dir_base = Path(iblrig.__file__).parents[1] - branch = check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], - cwd=dir_base) - branch = sub(r'\n', '', branch.decode()) - check_call(["git", "fetch", "origin", branch, "-q"], - cwd=dir_base, timeout=5) - version_str = check_output(["git", "describe", "--tags", "--abbrev=0"], - cwd=dir_base) - except (SubprocessError, FileNotFoundError): - return None - version_str = sub(r'[^\d\.]', '', version_str.decode()) - try: - Remote._version = version.parse(version_str) - return Remote._version - except (version.InvalidVersion, TypeError): - return None +@static_vars(is_fetched_already=False) +def fetch_remote_tags() -> None: + if fetch_remote_tags.is_fetched_already: + return + if not IS_GIT: + log.error('This installation of iblrig is not managed through git') + try: + branch = check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=BASE_DIR, timeout=5, text=True).removesuffix('\n') + check_call(["git", "fetch", "origin", branch, "-t", "-q"], cwd=BASE_DIR, timeout=5) + except (SubprocessError, CalledProcessError): + return + fetch_remote_tags.is_fetched_already = True -def is_git(): - return Path(iblrig.__file__).parents[1].joinpath('.git').exists() +@static_vars(remote_version=None) +def get_remote_version() -> Union[version.Version, None]: + """ + Retrieve the remote version of iblrig from the Git repository. + This function fetches and parses the remote version of the iblrig software + from the Git repository. It uses Git tags to identify available versions. + + Returns + ------- + Union[version.Version, None] + A Version object representing the remote version if successfully obtained, + or None if the remote version cannot be retrieved. + + Notes + ----- + This method will only work with installations managed through Git. + """ + if get_remote_version.remote_version: + log.debug(f'Using cached remote version: {get_remote_version.remote_version}') + return get_remote_version.remote_version + + if not IS_GIT: + log.error('This installation of iblrig is not managed through git - cannot obtain remote version') + return None + + try: + log.debug('Obtaining remote version from github') + fetch_remote_tags() + references = check_output(["git", "ls-remote", "-t", "-q", "--exit-code", "--refs", "origin", "tags", "*"], + cwd=BASE_DIR, timeout=5, encoding='UTF-8') -def upgrade(): - if not is_git(): + except (SubprocessError, CalledProcessError, FileNotFoundError): + log.error('Could not obtain remote version string') + return None + + try: + log.debug('Parsing local version string') + get_remote_version.remote_version = max([version.parse(v) for v in re.findall(r'/(\d+\.\d+\.\d+)', references)]) + return get_remote_version.remote_version + except (version.InvalidVersion, TypeError): + log.error('Could not parse remote version string') + return None + + +def upgrade() -> int: + """ + Upgrade the IBLRIG software installation. + + This function upgrades the IBLRIG software installation to the latest version + available in the Git repository. It checks the local and remote versions, + confirms the upgrade with the user if necessary, and performs the upgrade. + + Returns + ------- + int + 0 if the upgrade process is successfully completed. + + Raises + ------ + Exception + - If the installation is not managed through Git. + - If the upgrade is attempted outside the IBLRIG virtual environment. + - If the local version cannot be obtained. + - If the remote version cannot be obtained. + + Notes + ----- + This method requires that the installation is managed through Git and that + the user is in the IBLRIG virtual environment. + """ + if not IS_GIT: raise Exception('This installation of IBLRIG is not managed through git.') if sys.base_prefix == sys.prefix: raise Exception('You need to be in the IBLRIG venv in order to upgrade.') - if not Remote.version(): + + try: + v_local = get_local_version() + assert v_local + except AssertionError: + raise Exception('Could not obtain local version.') + + try: + v_remote = get_remote_version() + assert v_remote + except AssertionError: raise Exception('Could not obtain remote version.') - local_version = version.parse(iblrig.__version__) - remote_version = Remote.version() - - print(f'Local version: {local_version}') - print(f'Remote version: {remote_version}') - - if local_version >= remote_version: - print('No need to upgrade.') - return 0 - - if iblrig.__version__.endswith('+dirty'): - print('There are changes in your local copy of IBLRIG that will be lost when ' - 'upgrading.') - while True: - user_input = input('Do you want to proceed? [y, N] ') - if user_input.lower() in ['n', 'no', '']: - return - if user_input.lower() in ['y', 'yes']: - check_call([sys.executable, "-m", "pip", "reset", "--hard"]) - break - - check_call([sys.executable, "git", "pull"]) - check_call([sys.executable, "-m", "pip", "install", "-U", "-e", "."]) + print(f'Local version: {v_local}') + print(f'Remote version: {v_remote}\n') + + if v_local >= v_remote: + if not _ask_user('No need to upgrade. Do you want to run the upgrade routine anyways?', False): + return 0 + + if v_local.local == 'dirty': + print('There are changes in your local copy of IBLRIG that will be lost when upgrading.') + if not _ask_user('Do you want to proceed?', False): + return 0 + check_call([sys.executable, "-m", "pip", "reset", "--hard"]) + + # check_call([sys.executable, "git", "pull", "--tags"]) + # check_call([sys.executable, "-m", "pip", "install", "-U", "-e", "."]) + + +def _ask_user(prompt: str, default: bool = False) -> bool: + """ + Prompt the user for a yes/no response. + + This function displays a prompt to the user and expects a yes or no response. + The response is not case-sensitive. If the user presses Enter without + typing anything, the function interprets it as the default response. + + Parameters + ---------- + prompt : str + The prompt message to display to the user. + default : bool, optional + The default response when the user presses Enter without typing + anything. If True, the default response is 'yes' (Y/y or Enter). + If False, the default response is 'no' (N/n or Enter). + + Returns + ------- + bool + True if the user responds with 'yes' + False if the user responds with 'no' + """ + while True: + user_input = input(f'{prompt} [Y/n] ' if default else f'{prompt} [y/N] ').strip().lower() + if not user_input: + return default + elif user_input in ['y', 'yes']: + return True + elif user_input in ['n', 'no']: + return False diff --git a/iblrigv8.code-workspace b/iblrigv8.code-workspace new file mode 100644 index 000000000..57097327f --- /dev/null +++ b/iblrigv8.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} diff --git a/pyproject.toml b/pyproject.toml index 00b7f219f..6f1bcff00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools>=64", - "setuptools_scm[toml]>=7.1", "wheel" ] build-backend = "setuptools.build_meta" @@ -33,7 +32,6 @@ dependencies = [ "PySocks", "PyYAML", "scipy", - "setuptools_scm", "sounddevice", ] @@ -60,10 +58,7 @@ upgrade_iblrig = "iblrig.version_management:upgrade" [tool.setuptools.dynamic] readme = {file = "README.md", content-type = "text/markdown"} - -[tool.setuptools_scm] -version_scheme = "post-release" -local_scheme = "dirty-tag" +version = {attr = "iblrig.__version__"} [tool.setuptools.packages] find = {}