merge
This commit is contained in:
commit
9a81ddfbcf
51
.github/workflows/python-app_linux.yml
vendored
Normal file
51
.github/workflows/python-app_linux.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: BlipBlop app linux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyqt5 pyinstaller flake8
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
- name: create package
|
||||
run: |
|
||||
pyrcc5 resources.qrc -o resources.py
|
||||
pyinstaller blipblop_linux.spec
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
# Artifact name
|
||||
name: BlibBlop-linux
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: |
|
||||
dist
|
||||
retention-days: 1
|
||||
|
||||
|
6
.github/workflows/python-app_macos.yml
vendored
6
.github/workflows/python-app_macos.yml
vendored
@ -1,7 +1,7 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python application
|
||||
name: BlipBlop app macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -36,13 +36,13 @@ jobs:
|
||||
- name: create package
|
||||
run: |
|
||||
pyrcc5 resources.qrc -o resources.py
|
||||
pyinstaller --onefile --windowed --name="BlipBlop" --osx-bundle-identifier="de.uni-tuebingen.neuroetho.blipblop" -i icons/blipblop_logo.icns --add-data="icons/blipblop_logo.icns:." --add-data="resources.py:." blipblop.py
|
||||
pyinstaller --osx-bundle-identifier="de.uni-tuebingen.neuroetho.blipblop" blipblop_darwin.spec
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
# Artifact name
|
||||
name: blipblop-binary
|
||||
name: BlipBlop-macOS
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: |
|
||||
dist
|
||||
|
5
.github/workflows/python-app_win.yml
vendored
5
.github/workflows/python-app_win.yml
vendored
@ -1,7 +1,7 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python application
|
||||
name: BlipBlop app windows
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -24,7 +24,6 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyqt5 pyinstaller flake8
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
@ -36,7 +35,7 @@ jobs:
|
||||
- name: create package
|
||||
run: |
|
||||
pyrcc5 resources.qrc -o resources.py
|
||||
pyinstaller --onefile --windowed --name="BlipBlop" -i icons/blipblop_logo.ico --add-data="icons/blipblop_logo.icns;." --add-data="resources.py;." blipblop.py
|
||||
pyinstaller blipblop_win.spec
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
*.pyc
|
||||
*__pycache__
|
||||
resources.py
|
||||
build
|
||||
dist
|
@ -4,36 +4,47 @@ from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtMultimedia import QMediaContent
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
import resources
|
||||
import resources # needs to be imported somewhere in the project to be picked up by qt
|
||||
|
||||
organization = "neuroetho.uni-tuebingen.de"
|
||||
application = "blipblop"
|
||||
version = 0.1
|
||||
organization_name = "de.uni-tuebingen.neuroetho"
|
||||
application_name = "BlipBlop"
|
||||
application_version = 0.1
|
||||
|
||||
PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
ICONS_FOLDER = os.path.join(PACKAGE_ROOT, "icons")
|
||||
DOCS_ROOT_FILE = os.path.join(PACKAGE_ROOT, "docs", "index.md")
|
||||
SNDS_FOLDER = os.path.join(PACKAGE_ROOT, "sounds")
|
||||
|
||||
# Find and list sounds
|
||||
SNDS_PATHS = glob.glob(os.path.join(SNDS_FOLDER, "*.wav"))
|
||||
SNDS_PATHS = sorted(SNDS_PATHS)
|
||||
SNDS_DICT = {}
|
||||
|
||||
for snd in SNDS_PATHS:
|
||||
SNDS_DICT[snd.split(os.sep)[-1].split(".")[0]] = snd
|
||||
|
||||
""" This snippet is kept because it shows how to iterate the qt resources.py file
|
||||
it = QDirIterator(":", QDirIterator.Subdirectories);
|
||||
while it.hasNext():
|
||||
name = it.next()
|
||||
if "sounds/" in name:
|
||||
SNDS_DICT[name.split("/")[-1]] = "qrc" + name
|
||||
"""
|
||||
|
||||
# Find and list icons and images
|
||||
ICONS_PATHS = glob.glob(os.path.join(ICONS_FOLDER, "*.png"))
|
||||
ICONS_PATHS.extend(glob.glob(os.path.join(ICONS_FOLDER, "*.icns")))
|
||||
ICONS_PATHS = sorted(ICONS_PATHS)
|
||||
ICON_DICT = {}
|
||||
|
||||
SNDS_PATHS = glob.glob(os.path.join(SNDS_FOLDER, "*.wav"))
|
||||
SNDS_PATHS = sorted(SNDS_PATHS)
|
||||
SNDS_DICT = {}
|
||||
|
||||
for icon in ICONS_PATHS:
|
||||
ICON_DICT[icon.split(os.sep)[-1].split(".")[0]] = icon
|
||||
|
||||
for snd in SNDS_PATHS:
|
||||
SNDS_DICT[snd.split(os.sep)[-1].split(".")[0]] = snd
|
||||
|
||||
|
||||
def get_sound(name):
|
||||
if name in SNDS_DICT.keys():
|
||||
return QMediaContent(QUrl.fromLocalFile(os.path.abspath(SNDS_DICT[name])))
|
||||
url = QUrl.fromLocalFile(SNDS_DICT[name])
|
||||
return QMediaContent(url)
|
||||
else:
|
||||
print("Sound %s not found!" % name)
|
||||
return None
|
||||
@ -43,5 +54,4 @@ def get_icon(name):
|
||||
if name in ICON_DICT.keys():
|
||||
return QIcon(ICON_DICT[name])
|
||||
else:
|
||||
return QIcon("nix_logo.png")
|
||||
|
||||
return QIcon("blipblop_logo.png")
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/python3
|
||||
import sys
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import QSettings
|
||||
from blipblop.ui.mainwindow import BlipBlop
|
||||
@ -8,22 +8,22 @@ import blipblop.constants as cnst
|
||||
try:
|
||||
# Include in try/except block if you're also targeting Mac/Linux
|
||||
from PyQt5.QtWinExtras import QtWin
|
||||
myappid = 'neuroetho.uni-tuebingen.de.blipblop.0.1'
|
||||
myappid = "%s.%s" %(cnst.organization_name, cnst.application_version)
|
||||
QtWin.setCurrentProcessExplicitAppUserModelID(myappid)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("blipblop")
|
||||
app.setApplicationVersion("0.1")
|
||||
app.setOrganizationDomain("neuroetho.uni-tuebingen.de")
|
||||
app.setWindowIcon(cnst.get_icon("blipblop_logo.png"))
|
||||
app.setApplicationName(cnst.application_name)
|
||||
app.setApplicationVersion(str(cnst.application_version))
|
||||
app.setOrganizationDomain(cnst.organization_name)
|
||||
app.setWindowIcon(QIcon(":/icons/app_icon_png"))
|
||||
settings = QSettings()
|
||||
width = settings.value("app/width", 1024)
|
||||
height = settings.value("app/height", 768)
|
||||
x = settings.value("app/pos_x", 100)
|
||||
y = settings.value("app/pos_y", 100)
|
||||
width = int(settings.value("app/width", 1024))
|
||||
height = int(settings.value("app/height", 768))
|
||||
x = int(settings.value("app/pos_x", 100))
|
||||
y = int(settings.value("app/pos_y", 100))
|
||||
window = BlipBlop()
|
||||
window.setMinimumWidth(800)
|
||||
window.setMinimumHeight(600)
|
||||
@ -38,7 +38,3 @@ def main():
|
||||
settings.setValue("app/pos_x", pos.x())
|
||||
settings.setValue("app/pos_y", pos.y())
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,105 +1,13 @@
|
||||
from PyQt5.QtWidgets import QAction, QComboBox, QFormLayout, QGridLayout, QLabel, QPushButton, QSizePolicy, QSlider, QSpinBox, QSplitter, QTextEdit, QVBoxLayout, QWidget
|
||||
from PyQt5.QtWidgets import QAction, QGridLayout, QLabel, QPushButton, QSizePolicy, QSplitter, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QPoint, QRandomGenerator, QTimer, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QFont, QIcon, QKeySequence, QPainter, QPen, QPixmap
|
||||
from PyQt5.QtMultimedia import QMediaPlayer
|
||||
|
||||
import os
|
||||
import blipblop.constants as cnst
|
||||
from blipblop.ui.countdownlabel import CountdownLabel
|
||||
from blipblop.ui.settings import AuditoryTaskSettings
|
||||
import datetime as dt
|
||||
|
||||
class SettingsPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._trial_spinner = QSpinBox()
|
||||
self._trial_spinner.setMinimum(5)
|
||||
self._trial_spinner.setMaximum(25)
|
||||
self._trial_spinner.setValue(5)
|
||||
self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)")
|
||||
|
||||
self._min_delay_spinner = QSpinBox()
|
||||
self._min_delay_spinner.setMinimum(1)
|
||||
self._min_delay_spinner.setMaximum(10)
|
||||
self._min_delay_spinner.setValue(1)
|
||||
self._min_delay_spinner.setToolTip("Minimum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._max_delay_spinner = QSpinBox()
|
||||
self._max_delay_spinner.setMinimum(1)
|
||||
self._max_delay_spinner.setMaximum(10)
|
||||
self._max_delay_spinner.setValue(5)
|
||||
self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._countdown_spinner = QSpinBox()
|
||||
self._countdown_spinner.setMinimum(1)
|
||||
self._countdown_spinner.setMaximum(10)
|
||||
self._countdown_spinner.setValue(3)
|
||||
self._countdown_spinner.setToolTip("Pause between trials [s]")
|
||||
|
||||
self._saliency_slider = QSlider(Qt.Horizontal)
|
||||
self._saliency_slider.setMinimum(0)
|
||||
self._saliency_slider.setMaximum(100)
|
||||
self._saliency_slider.setSliderPosition(100)
|
||||
self._saliency_slider.setTickInterval(25)
|
||||
self._saliency_slider.setTickPosition(QSlider.TicksBelow)
|
||||
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its loudness")
|
||||
|
||||
self._sound_combo = QComboBox()
|
||||
for k in cnst.SNDS_DICT.keys():
|
||||
self._sound_combo.addItem(k)
|
||||
|
||||
self._instructions = QTextEdit()
|
||||
self._instructions.setMarkdown("* fixate central cross\n * press start (enter) when ready\n * press space bar as soon as the stimulus occurs")
|
||||
self._instructions.setMinimumHeight(200)
|
||||
self._instructions.setReadOnly(True)
|
||||
|
||||
form_layout = QFormLayout()
|
||||
form_layout.addRow("Settings", None)
|
||||
form_layout.addRow("number of trials", self._trial_spinner)
|
||||
form_layout.addRow("minimum delay [s]", self._min_delay_spinner)
|
||||
form_layout.addRow("maximum delay [s]", self._max_delay_spinner)
|
||||
form_layout.addRow("pause [s]", self._countdown_spinner)
|
||||
form_layout.addRow("stimulus saliency", self._saliency_slider)
|
||||
form_layout.addRow("stimulus sound", self._sound_combo)
|
||||
form_layout.addRow("instructions", self._instructions)
|
||||
self.setLayout(form_layout)
|
||||
|
||||
@property
|
||||
def trials(self):
|
||||
return self._trial_spinner.value()
|
||||
|
||||
@property
|
||||
def saliency(self):
|
||||
return self._saliency_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def min_delay(self):
|
||||
return self._min_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def max_delay(self):
|
||||
return self._max_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def countdown(self):
|
||||
return self._countdown_spinner.value()
|
||||
|
||||
@property
|
||||
def sound(self):
|
||||
return self._sound_combo.currentText()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._trial_spinner.setEnabled(enabled)
|
||||
self._saliency_slider.setEnabled(enabled)
|
||||
self._countdown_spinner.setEnabled(enabled)
|
||||
self._min_delay_spinner.setEnabled(enabled)
|
||||
self._max_delay_spinner.setEnabled(enabled)
|
||||
self._sound_combo.setEnabled(False)
|
||||
|
||||
|
||||
class AudioBlop(QWidget):
|
||||
task_done = pyqtSignal()
|
||||
@ -116,17 +24,17 @@ class AudioBlop(QWidget):
|
||||
grid.setRowStretch(3, 1)
|
||||
widget.setLayout(grid)
|
||||
|
||||
l = QLabel("Auditory reaction test")
|
||||
l.setPixmap(QPixmap(":/icons/auditory_task"))
|
||||
grid.addWidget(l, 0, 0, Qt.AlignLeft)
|
||||
icon_label = QLabel("Auditory reaction test")
|
||||
icon_label.setPixmap(QPixmap(":/icons/auditory_task"))
|
||||
grid.addWidget(icon_label, 0, 0, Qt.AlignLeft)
|
||||
|
||||
l2 =QLabel("Measurement of auditory reaction times\npress enter to start")
|
||||
heading_label = QLabel("Measurement of auditory reaction times\npress enter to start")
|
||||
font = QFont()
|
||||
font.setBold(True)
|
||||
font.setPointSize(20)
|
||||
l2.setFont(font)
|
||||
l2.setStyleSheet("color: #2D4B9A")
|
||||
grid.addWidget(l2, 1, 0, 1, 2, Qt.AlignLeft)
|
||||
heading_label.setFont(font)
|
||||
heading_label.setStyleSheet("color: #2D4B9A")
|
||||
grid.addWidget(heading_label, 1, 0, 1, 2, Qt.AlignLeft)
|
||||
|
||||
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
||||
settings_btn.setToolTip("edit task settings")
|
||||
@ -135,17 +43,17 @@ class AudioBlop(QWidget):
|
||||
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
||||
|
||||
self._status_label = QLabel("Ready to start, press enter ...")
|
||||
grid.addWidget(self._status_label, 3, 0, Qt.AlignLeft)
|
||||
grid.addWidget(self._status_label, 4, 0, Qt.AlignLeft)
|
||||
|
||||
self._countdown_label = CountdownLabel(text="Next trial in:")
|
||||
grid.addWidget(self._countdown_label, 3, 1, Qt.AlignCenter)
|
||||
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
||||
self._countdown_label.countdown_done.connect(self.run_trial)
|
||||
|
||||
self._draw_area = QLabel()
|
||||
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
grid.addWidget(self._draw_area, 2, 1)
|
||||
|
||||
self._settings = SettingsPanel()
|
||||
self._settings = AuditoryTaskSettings()
|
||||
|
||||
self._splitter = QSplitter()
|
||||
self._splitter.addWidget(widget)
|
||||
@ -206,7 +114,6 @@ class AudioBlop(QWidget):
|
||||
return
|
||||
self._countdown_label.start(self._settings.countdown)
|
||||
|
||||
|
||||
def reset_canvas(self):
|
||||
bkg_color = QColor()
|
||||
bkg_color.setAlphaF(0.0)
|
||||
@ -240,6 +147,7 @@ class AudioBlop(QWidget):
|
||||
return
|
||||
if not self._session_running:
|
||||
self._settings.set_enabled(False)
|
||||
self._settings.store_settings()
|
||||
self._session_running = True
|
||||
self._countdown_label.start(time=self._settings.countdown)
|
||||
|
||||
@ -251,6 +159,7 @@ class AudioBlop(QWidget):
|
||||
self._trial_counter += 1
|
||||
self._status_label.setText("Trial %i of %i running" % (self._trial_counter, self._settings.trials))
|
||||
content = cnst.get_sound(self._settings.sound)
|
||||
|
||||
self._player.setMedia(content)
|
||||
self._player.setVolume(self._settings.saliency)
|
||||
|
||||
|
@ -52,6 +52,7 @@ class CentralWidget(QWidget):
|
||||
|
||||
def reset(self):
|
||||
self._task_results = []
|
||||
self._results_screen.reset()
|
||||
self._stack.setCurrentIndex(0)
|
||||
|
||||
def on_new_visual_task(self):
|
||||
|
@ -1,142 +0,0 @@
|
||||
from PyQt5.QtWidgets import QComboBox, QFrame, QGroupBox, QHBoxLayout, QLabel, QSplitter, QTextEdit, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QItemSelectionModel, Qt
|
||||
|
||||
from nixview.util.file_handler import FileHandler
|
||||
from nixview.util.descriptors import ItemDescriptor
|
||||
import nixview.communicator as comm
|
||||
import nixview.constants as cnst
|
||||
from nixview.data_models.tree_model import NixTreeView, TreeModel, TreeType
|
||||
|
||||
|
||||
class FileScreen(QWidget):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._file_handler = FileHandler()
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
self.setLayout(vbox)
|
||||
|
||||
main_splitter = QSplitter(Qt.Vertical)
|
||||
self.layout().addWidget(main_splitter)
|
||||
|
||||
self._info = EntityInfo(self)
|
||||
main_splitter.addWidget(self._info)
|
||||
|
||||
self._data_tree = NixTreeView(self)
|
||||
|
||||
self._tree_type_combo = QComboBox()
|
||||
self._tree_type_combo.adjustSize()
|
||||
self._tree_type_combo.addItems([TreeType.Data.value, TreeType.Full.value, TreeType.Metadata.value])
|
||||
self._tree_type_combo.currentTextChanged.connect(self.update)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(QLabel("Tree type:"))
|
||||
hbox.addWidget(self._tree_type_combo)
|
||||
hbox.addStretch()
|
||||
data_group = QGroupBox("Data")
|
||||
data_vbox = QVBoxLayout()
|
||||
data_vbox.setContentsMargins(1, 10, 1, 1)
|
||||
|
||||
data_vbox.addLayout(hbox)
|
||||
data_vbox.addWidget(self._data_tree)
|
||||
data_group.setLayout(data_vbox)
|
||||
|
||||
main_splitter.addWidget(data_group)
|
||||
main_splitter.setSizes([200, 600])
|
||||
vbox.addWidget(main_splitter)
|
||||
|
||||
def dataTreeSelection(self, current_index, last_index):
|
||||
if not current_index.isValid():
|
||||
return
|
||||
item = current_index.internalPointer()
|
||||
comm.communicator.item_selected.emit(item)
|
||||
self._info.setEntityInfo(item.node_descriptor)
|
||||
|
||||
def update(self):
|
||||
tt = TreeType.Data
|
||||
if self._tree_type_combo.currentText() == TreeType.Data.value:
|
||||
tt = TreeType.Data
|
||||
elif self._tree_type_combo.currentText() == TreeType.Full.value:
|
||||
tt = TreeType.Full
|
||||
elif self._tree_type_combo.currentText() == TreeType.Metadata.value:
|
||||
tt = TreeType.Metadata
|
||||
self._info.setEntityInfo(None)
|
||||
data_model = TreeModel(self._file_handler, tt)
|
||||
self._data_tree.setModel(data_model)
|
||||
selection_model = QItemSelectionModel(data_model)
|
||||
self._data_tree.setSelectionModel(selection_model)
|
||||
selection_model.currentChanged.connect(self.dataTreeSelection)
|
||||
for i in range(data_model.columnCount(None)):
|
||||
self._data_tree.resizeColumnToContents(i)
|
||||
self._info.setFileInfo(self._file_handler.file_descriptor)
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
|
||||
class EntityInfo(QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent=parent)
|
||||
self._file_handler = FileHandler()
|
||||
self.setLayout(QHBoxLayout())
|
||||
|
||||
self._metadata_tree = NixTreeView()
|
||||
|
||||
mdata_grp = QGroupBox("Metadata")
|
||||
mdata_grp.setLayout(QVBoxLayout())
|
||||
mdata_grp.layout().setContentsMargins(1, 10, 1, 1)
|
||||
mdata_grp.layout().addWidget(self._metadata_tree)
|
||||
|
||||
file_info_grp = QGroupBox("File info")
|
||||
file_info_grp.setLayout(QVBoxLayout())
|
||||
file_info_grp.layout().setContentsMargins(1, 10, 1, 1)
|
||||
self._file_info = QTextEdit("File information")
|
||||
self._file_info.setEnabled(True)
|
||||
self._file_info.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
|
||||
self._file_info.setFrameShape(QFrame.NoFrame)
|
||||
self._file_info.setLineWrapMode(QTextEdit.WidgetWidth)
|
||||
file_info_grp.layout().addWidget(self._file_info)
|
||||
|
||||
entity_info_grp = QGroupBox("Entity info")
|
||||
entity_info_grp.setLayout(QVBoxLayout())
|
||||
entity_info_grp.layout().setContentsMargins(1, 10, 1, 1)
|
||||
self._entity_info = QTextEdit("Entity information")
|
||||
self._file_info.setEnabled(True)
|
||||
self._file_info.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
|
||||
self._file_info.setFrameShape(QFrame.NoFrame)
|
||||
self._file_info.setLineWrapMode(QTextEdit.WidgetWidth)
|
||||
entity_info_grp.layout().addWidget(self._entity_info)
|
||||
|
||||
self._splitter = QSplitter(Qt.Horizontal)
|
||||
self._splitter.addWidget(file_info_grp)
|
||||
self._splitter.addWidget(entity_info_grp)
|
||||
self._splitter.addWidget(mdata_grp)
|
||||
self._splitter.setSizes([200, 400, 0])
|
||||
self._splitter.setStretchFactor(0, 0)
|
||||
self._splitter.setStretchFactor(1, 1)
|
||||
self._splitter.setStretchFactor(2, 1)
|
||||
|
||||
self.layout().addWidget(self._splitter)
|
||||
|
||||
|
||||
def setFileInfo(self, file_info):
|
||||
if file_info is not None:
|
||||
self._file_info.setText(file_info.toHtml())
|
||||
|
||||
def setEntityInfo(self, entity_info):
|
||||
if entity_info is None or not isinstance(entity_info, ItemDescriptor):
|
||||
self._splitter.setSizes([200, 400, 0])
|
||||
self._entity_info.setText("")
|
||||
self._metadata_tree.setModel(None)
|
||||
return
|
||||
|
||||
if entity_info.metadata_id is not None:
|
||||
self._splitter.setSizes([200, 400, 400])
|
||||
else:
|
||||
self._splitter.setSizes([200, 400, 0])
|
||||
self._entity_info.setText(entity_info.to_html())
|
||||
metadata_model = TreeModel(self._file_handler, TreeType.Metadata, root_section_id=entity_info.metadata_id)
|
||||
self._metadata_tree.setModel(metadata_model)
|
||||
for i in range(metadata_model.columnCount(None)):
|
||||
self._metadata_tree.resizeColumnToContents(i)
|
@ -1,27 +1,37 @@
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QPushButton, QSizePolicy, QTextBrowser, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QFrame, QHBoxLayout
|
||||
from PyQt5.QtWidgets import QPushButton, QSizePolicy, QTextBrowser, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QSettings, QUrl, Qt
|
||||
|
||||
import blipblop.constants as cnst
|
||||
|
||||
|
||||
class HelpDialog(QDialog):
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._settings = QSettings()
|
||||
width = int(self._settings.value("help/width", 640))
|
||||
height = int(self._settings.value("help/height", 480))
|
||||
x = int(self._settings.value("help/pos_x", 100))
|
||||
y = int(self._settings.value("help/pos_y", 100))
|
||||
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(500, 750)
|
||||
|
||||
self.setMinimumSize(640, 480)
|
||||
self.resize(width, height)
|
||||
self.move(x, y)
|
||||
self.finished.connect(self.on_finished)
|
||||
self.help = HelpBrowser()
|
||||
|
||||
self.help._edit.historyChanged.connect(self._on_history_changed)
|
||||
|
||||
self.back_btn = QPushButton(QIcon(":/icons/back_btn"), "back")
|
||||
self.back_btn = QPushButton(QIcon(":/icons/docs_back"), "back")
|
||||
self.back_btn.setEnabled(False)
|
||||
self.back_btn.clicked.connect(self.help._edit.backward)
|
||||
self.home_btn = QPushButton(QIcon(":/icons/home_btn"),"home")
|
||||
self.home_btn = QPushButton(QIcon(":/icons/docs_home"), "home")
|
||||
self.home_btn.clicked.connect(self.help._edit.home)
|
||||
self.fwd_btn = QPushButton(QIcon(":/icons/fwd_btn"),"forward")
|
||||
self.fwd_btn = QPushButton(QIcon(":/icons/docs_forward"), "forward")
|
||||
self.fwd_btn.setLayoutDirection(Qt.RightToLeft)
|
||||
self.fwd_btn.setEnabled(False)
|
||||
self.fwd_btn.clicked.connect(self.help._edit.forward)
|
||||
|
||||
@ -47,12 +57,18 @@ class HelpDialog(QDialog):
|
||||
self.back_btn.setEnabled(self.help._edit.isBackwardAvailable())
|
||||
self.fwd_btn.setEnabled(self.help._edit.isForwardAvailable())
|
||||
|
||||
def on_finished(self):
|
||||
self._settings.setValue("help/width", self.width())
|
||||
self._settings.setValue("help/height", self.height())
|
||||
self._settings.setValue("help/pos_x", self.x())
|
||||
self._settings.setValue("help/pos_y", self.y())
|
||||
|
||||
|
||||
class HelpBrowser(QWidget):
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self.setLayout(QVBoxLayout())
|
||||
# FIXME https://stackoverflow.com/a/43217828 about loading from esource files
|
||||
doc_url = QUrl.fromLocalFile(cnst.DOCS_ROOT_FILE)
|
||||
self._edit = QTextBrowser()
|
||||
self._edit.setOpenLinks(True)
|
||||
|
@ -24,17 +24,17 @@ class BlipBlop(QMainWindow):
|
||||
self.show()
|
||||
|
||||
def create_actions(self):
|
||||
self._quit_action = QAction(QIcon(":/icons/quit"), "Quit", self)
|
||||
self._quit_action = QAction(QIcon(":/icons/quit"), "quit application", self)
|
||||
self._quit_action.setStatusTip("Quit BlipBlop")
|
||||
self._quit_action.setShortcut(QKeySequence("Ctrl+q"))
|
||||
self._quit_action.triggered.connect(self.on_quit)
|
||||
|
||||
self._new_action = QAction(QIcon(":/icons/new_session"), "New session", self)
|
||||
self._new_action = QAction(QIcon(":/icons/new_session"), "new session", self)
|
||||
self._new_action.setStatusTip("Start a new session discarding previous results")
|
||||
self._new_action.setShortcut(QKeySequence("Ctrl+n"))
|
||||
self._new_action.triggered.connect(self.on_new)
|
||||
|
||||
self._results_action = QAction(QIcon(":/icons/results_table"), "Show results", self)
|
||||
self._results_action = QAction(QIcon(":/icons/results_table"), "show results", self)
|
||||
self._results_action.setStatusTip("Show results as table")
|
||||
self._results_action.setShortcut(QKeySequence("Ctrl+r"))
|
||||
self._results_action.setEnabled(True)
|
||||
@ -51,13 +51,13 @@ class BlipBlop(QMainWindow):
|
||||
self._help_action.setEnabled(True)
|
||||
self._help_action.triggered.connect(self.on_help)
|
||||
|
||||
self._visual_task_action = QAction(QIcon(":/icons/visual_task"), "visual")
|
||||
self._visual_task_action = QAction(QIcon(":/icons/visual_task"), "visual task")
|
||||
self._visual_task_action.setStatusTip("Start measuring visual reaction times")
|
||||
self._visual_task_action.setShortcut(QKeySequence("Ctrl+1"))
|
||||
self._visual_task_action.setEnabled(True)
|
||||
self._visual_task_action.triggered.connect(self.on_visual)
|
||||
|
||||
self._auditory_task_action = QAction(QIcon(":/icons/auditory_task"), "auditory")
|
||||
self._auditory_task_action = QAction(QIcon(":/icons/auditory_task"), "auditory task")
|
||||
self._auditory_task_action.setStatusTip("Start measuring auditory reaction times")
|
||||
self._auditory_task_action.setShortcut(QKeySequence("Ctrl+2"))
|
||||
self._auditory_task_action.setEnabled(True)
|
||||
@ -77,6 +77,7 @@ class BlipBlop(QMainWindow):
|
||||
self._toolbar.addAction(self._visual_task_action)
|
||||
self._toolbar.addAction(self._auditory_task_action)
|
||||
self._toolbar.addAction(self._results_action)
|
||||
self._toolbar.addSeparator()
|
||||
self._toolbar.addAction(self._help_action)
|
||||
|
||||
empty = QWidget()
|
||||
|
@ -1,11 +1,10 @@
|
||||
import io
|
||||
import csv
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtGui import QFont, QKeySequence
|
||||
from PyQt5.QtGui import QFont, QIcon, QKeySequence
|
||||
from PyQt5.QtWidgets import QAction, QLabel, QStackedLayout, QTableWidget, QTableWidgetItem, QWidget
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
import blipblop.constants as cnst
|
||||
from blipblop.util.results import MeasurementResults
|
||||
|
||||
|
||||
@ -54,7 +53,7 @@ class ResultsScreen(QWidget):
|
||||
self.table.setColumnCount(col_count)
|
||||
|
||||
for col, mr in enumerate(measurement_results):
|
||||
headerItem = QTableWidgetItem(cnst.get_icon("visual_task") if "visual" in mr.name.lower() else cnst.get_icon("auditory_task"), "")
|
||||
headerItem = QTableWidgetItem(QIcon(":/icons/visual_task") if "visual" in mr.name.lower() else QIcon(":/icons/auditory_task"), "")
|
||||
headerItem.setToolTip("%s started at\n %s " % (mr.name, mr.starttime))
|
||||
self.table.setHorizontalHeaderItem(col, headerItem)
|
||||
for row, r in enumerate(mr.results):
|
||||
@ -89,4 +88,3 @@ class ResultsScreen(QWidget):
|
||||
def reset(self):
|
||||
self.table.clear()
|
||||
self._stack.setCurrentIndex(0)
|
||||
|
||||
|
148
blipblop/ui/settings.py
Normal file
148
blipblop/ui/settings.py
Normal file
@ -0,0 +1,148 @@
|
||||
from PyQt5.QtWidgets import QComboBox, QFormLayout, QSlider, QSpinBox, QTextEdit, QWidget
|
||||
from PyQt5.QtCore import Qt, QSettings
|
||||
import blipblop.constants as cnst
|
||||
|
||||
|
||||
class TaskSettings(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.settings = QSettings()
|
||||
|
||||
self._trial_spinner = QSpinBox()
|
||||
self._trial_spinner.setMinimum(5)
|
||||
self._trial_spinner.setMaximum(25)
|
||||
self._trial_spinner.setValue(int(self.settings.value("task/trials", 5)))
|
||||
self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)")
|
||||
|
||||
self._min_delay_spinner = QSpinBox()
|
||||
self._min_delay_spinner.setMinimum(1)
|
||||
self._min_delay_spinner.setMaximum(10)
|
||||
self._min_delay_spinner.setValue(int(self.settings.value("task/min_delay", 1)))
|
||||
self._min_delay_spinner.setToolTip("Minimum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._max_delay_spinner = QSpinBox()
|
||||
self._max_delay_spinner.setMinimum(1)
|
||||
self._max_delay_spinner.setMaximum(10)
|
||||
self._max_delay_spinner.setValue(int(self.settings.value("task/max_delay", 5)))
|
||||
self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._countdown_spinner = QSpinBox()
|
||||
self._countdown_spinner.setMinimum(1)
|
||||
self._countdown_spinner.setMaximum(10)
|
||||
self._countdown_spinner.setValue(int(self.settings.value("task/pause", 3)))
|
||||
self._countdown_spinner.setToolTip("Pause between trials [s]")
|
||||
|
||||
self._saliency_slider = QSlider(Qt.Horizontal)
|
||||
self._saliency_slider.setMinimum(0)
|
||||
self._saliency_slider.setMaximum(100)
|
||||
self._saliency_slider.setSliderPosition(int(self.settings.value("task/saliency", 100)))
|
||||
self._saliency_slider.setTickInterval(25)
|
||||
self._saliency_slider.setTickPosition(QSlider.TicksBelow)
|
||||
|
||||
self._instructions = QTextEdit()
|
||||
self._instructions.setMarkdown("* fixate central cross\n * press start (enter) when ready\n * press space bar as soon as the stimulus occurs")
|
||||
self._instructions.setMinimumHeight(200)
|
||||
self._instructions.setReadOnly(True)
|
||||
|
||||
self.form_layout = QFormLayout()
|
||||
self.form_layout.addRow("Settings", None)
|
||||
self.form_layout.addRow("number of trials", self._trial_spinner)
|
||||
self.form_layout.addRow("minimum delay [s]", self._min_delay_spinner)
|
||||
self.form_layout.addRow("maximum delay [s]", self._max_delay_spinner)
|
||||
self.form_layout.addRow("pause [s]", self._countdown_spinner)
|
||||
self.form_layout.addRow("stimulus saliency", self._saliency_slider)
|
||||
self.form_layout.addRow("instructions", self._instructions)
|
||||
self.setLayout(self.form_layout)
|
||||
|
||||
@property
|
||||
def trials(self):
|
||||
return self._trial_spinner.value()
|
||||
|
||||
@property
|
||||
def saliency(self):
|
||||
return self._saliency_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def min_delay(self):
|
||||
return self._min_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def max_delay(self):
|
||||
return self._max_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def countdown(self):
|
||||
return self._countdown_spinner.value()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._trial_spinner.setEnabled(enabled)
|
||||
self._saliency_slider.setEnabled(enabled)
|
||||
self._countdown_spinner.setEnabled(enabled)
|
||||
self._min_delay_spinner.setEnabled(enabled)
|
||||
self._max_delay_spinner.setEnabled(enabled)
|
||||
|
||||
def store_settings(self):
|
||||
self.settings.setValue("task/trials", self.trials)
|
||||
self.settings.setValue("task/salience", self.saliency)
|
||||
self.settings.setValue("task/min_delay", self.min_delay)
|
||||
self.settings.setValue("task/max_delay", self.max_delay)
|
||||
self.settings.setValue("task/pause", self.countdown)
|
||||
|
||||
|
||||
class AuditoryTaskSettings(TaskSettings):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its loudness")
|
||||
|
||||
self._sound_combo = QComboBox()
|
||||
for k in cnst.SNDS_DICT.keys():
|
||||
self._sound_combo.addItem(k)
|
||||
self._sound_combo.setCurrentIndex(int(self.settings.value("auditory_task/sound_index", 0)))
|
||||
self.form_layout.insertRow(self.form_layout.rowCount() - 1, "stimulus sound", self._sound_combo)
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
super().set_enabled(enabled)
|
||||
self._sound_combo.setEnabled(enabled)
|
||||
|
||||
def store_settings(self):
|
||||
super().store_settings()
|
||||
self.settings.setValue("auditory_task/sound_index", self._sound_combo.currentIndex())
|
||||
|
||||
@property
|
||||
def sound(self):
|
||||
return self._sound_combo.currentText()
|
||||
|
||||
|
||||
class VisualTaskSettings(TaskSettings):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its opacity")
|
||||
|
||||
self._size_slider = QSlider(Qt.Horizontal)
|
||||
self._size_slider.setMinimum(0)
|
||||
self._size_slider.setMaximum(200)
|
||||
self._size_slider.setSliderPosition(int(self.settings.value("visual_task/stimulus_size", 100)))
|
||||
self._size_slider.setTickInterval(25)
|
||||
self._size_slider.setTickPosition(QSlider.TicksBelow)
|
||||
self._size_slider.setToolTip("Diameter of the stimulus in pixel")
|
||||
|
||||
self.form_layout.insertRow(self.form_layout.rowCount() - 1, "stimulus size", self._size_slider)
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
super().set_enabled(enabled)
|
||||
self._size_slider.setEnabled(enabled)
|
||||
|
||||
def store_settings(self):
|
||||
super().store_settings()
|
||||
self.settings.setValue("visual_task/stimulus_size", self.size)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size_slider.sliderPosition()
|
@ -1,13 +1,11 @@
|
||||
import os
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
from PyQt5.QtGui import QFont, QPixmap
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
import blipblop.constants as cnst
|
||||
|
||||
|
||||
class MyLabel(QLabel):
|
||||
clicked = pyqtSignal()
|
||||
|
||||
def mouseReleaseEvent(self, QMouseEvent):
|
||||
if QMouseEvent.button() == Qt.LeftButton:
|
||||
self.clicked.emit()
|
||||
@ -38,19 +36,21 @@ class StartScreen(QWidget):
|
||||
layout.addWidget(label, 1, 2, 1, 3, Qt.AlignCenter)
|
||||
|
||||
visual_task_label = MyLabel()
|
||||
visual_task_label.setStatusTip("Start a new visual measurement (Ctrl+1)")
|
||||
visual_task_label.setToolTip("Click to start a new visual task (Ctrl+1)")
|
||||
visual_task_label.setPixmap(QPixmap(":/icons/visual_task_large"))
|
||||
visual_task_label.setMaximumWidth(256)
|
||||
visual_task_label.clicked.connect(self.new_visual_task)
|
||||
|
||||
auditory_task_label = MyLabel()
|
||||
auditory_task_label.setToolTip("Click to start a new auditory task (Ctrl+2)")
|
||||
auditory_task_label.setStatusTip("Start a new auditory measurement (Ctrl+2)")
|
||||
auditory_task_label.setToolTip("click to start a new auditory task (Ctrl+2)")
|
||||
auditory_task_label.setPixmap(QPixmap(":/icons/auditory_task_large"))
|
||||
auditory_task_label.setMaximumWidth(256)
|
||||
auditory_task_label.clicked.connect(self.new_auditory_task)
|
||||
|
||||
layout.addWidget(visual_task_label, 2, 1, 2, 2, Qt.AlignCenter )
|
||||
layout.addWidget(auditory_task_label, 2, 4, 2, 2, Qt.AlignCenter )
|
||||
layout.addWidget(visual_task_label, 2, 1, 2, 2, Qt.AlignCenter)
|
||||
layout.addWidget(auditory_task_label, 2, 4, 2, 2, Qt.AlignCenter)
|
||||
|
||||
def new_auditory_task(self):
|
||||
self.auditory_task_signal.emit()
|
||||
|
@ -1,104 +1,11 @@
|
||||
from PyQt5.QtWidgets import QAction, QFormLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QSlider, QSpinBox, QSplitter, QTextEdit, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QPoint, QRandomGenerator, QTimer, Qt, pyqtSignal, QSettings
|
||||
from PyQt5.QtGui import QColor, QFont, QKeySequence, QPainter, QBrush, QPen, QPixmap
|
||||
from PyQt5.QtWidgets import QAction, QGridLayout, QLabel, QPushButton, QSizePolicy, QSplitter, QVBoxLayout, QWidget
|
||||
from PyQt5.QtCore import QPoint, QRandomGenerator, QTimer, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QFont, QIcon, QKeySequence, QPainter, QBrush, QPen, QPixmap
|
||||
|
||||
import os
|
||||
import blipblop.constants as cnst
|
||||
from blipblop.ui.countdownlabel import CountdownLabel
|
||||
from blipblop.ui.settings import VisualTaskSettings
|
||||
import datetime as dt
|
||||
|
||||
class SettingsPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._trial_spinner = QSpinBox()
|
||||
self._trial_spinner.setMinimum(5)
|
||||
self._trial_spinner.setMaximum(25)
|
||||
self._trial_spinner.setValue(5)
|
||||
self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)")
|
||||
|
||||
self._min_delay_spinner = QSpinBox()
|
||||
self._min_delay_spinner.setMinimum(1)
|
||||
self._min_delay_spinner.setMaximum(10)
|
||||
self._min_delay_spinner.setValue(1)
|
||||
self._min_delay_spinner.setToolTip("Minimum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._max_delay_spinner = QSpinBox()
|
||||
self._max_delay_spinner.setMinimum(1)
|
||||
self._max_delay_spinner.setMaximum(10)
|
||||
self._max_delay_spinner.setValue(5)
|
||||
self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]")
|
||||
|
||||
self._saliency_slider = QSlider(Qt.Horizontal)
|
||||
self._saliency_slider.setMinimum(0)
|
||||
self._saliency_slider.setMaximum(100)
|
||||
self._saliency_slider.setSliderPosition(100)
|
||||
self._saliency_slider.setTickInterval(25)
|
||||
self._saliency_slider.setTickPosition(QSlider.TicksBelow)
|
||||
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its opacity")
|
||||
|
||||
self._size_slider = QSlider(Qt.Horizontal)
|
||||
self._size_slider.setMinimum(0)
|
||||
self._size_slider.setMaximum(200)
|
||||
self._size_slider.setSliderPosition(100)
|
||||
self._size_slider.setTickInterval(25)
|
||||
self._size_slider.setTickPosition(QSlider.TicksBelow)
|
||||
self._size_slider.setToolTip("Diameter of the stimulus in pixel")
|
||||
|
||||
self._countdown_spinner = QSpinBox()
|
||||
self._countdown_spinner.setMinimum(2)
|
||||
self._countdown_spinner.setMaximum(30)
|
||||
self._countdown_spinner.setValue(5)
|
||||
self._countdown_spinner.setToolTip("Pause/countdown for next trial")
|
||||
|
||||
self._instructions = QTextEdit()
|
||||
self._instructions.setMarkdown("* fixate central cross\n * press start (enter) when ready\n * press space bar as soon as the stimulus occurs")
|
||||
self._instructions.setMinimumHeight(200)
|
||||
self._instructions.setReadOnly(True)
|
||||
|
||||
form_layout = QFormLayout()
|
||||
form_layout.addRow("Settings", None)
|
||||
form_layout.addRow("number of trials", self._trial_spinner)
|
||||
form_layout.addRow("pause until next trial [s]", self._countdown_spinner)
|
||||
form_layout.addRow("minimum delay [s]", self._min_delay_spinner)
|
||||
form_layout.addRow("maximum delay [s]", self._max_delay_spinner)
|
||||
form_layout.addRow("stimulus saliency", self._saliency_slider)
|
||||
form_layout.addRow("stimulus size", self._size_slider)
|
||||
form_layout.addRow("instructions", self._instructions)
|
||||
self.setLayout(form_layout)
|
||||
|
||||
@property
|
||||
def trials(self):
|
||||
return self._trial_spinner.value()
|
||||
|
||||
@property
|
||||
def saliency(self):
|
||||
return self._saliency_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size_slider.sliderPosition()
|
||||
|
||||
@property
|
||||
def min_delay(self):
|
||||
return self._min_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def max_delay(self):
|
||||
return self._max_delay_spinner.value()
|
||||
|
||||
@property
|
||||
def countdown(self):
|
||||
return self._countdown_spinner.value()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._trial_spinner.setEnabled(enabled)
|
||||
self._saliency_slider.setEnabled(enabled)
|
||||
self._size_slider.setEnabled(enabled)
|
||||
self._countdown_spinner.setEnabled(enabled)
|
||||
self._min_delay_spinner.setEnabled(enabled)
|
||||
self._max_delay_spinner.setEnabled(enabled)
|
||||
|
||||
|
||||
class VisualBlip(QWidget):
|
||||
task_done = pyqtSignal()
|
||||
@ -115,36 +22,36 @@ class VisualBlip(QWidget):
|
||||
grid.setRowStretch(3, 1)
|
||||
widget.setLayout(grid)
|
||||
|
||||
l = QLabel("Visual reaction test")
|
||||
l.setPixmap(QPixmap(os.path.join(cnst.ICONS_FOLDER, "visual_task.png")))
|
||||
grid.addWidget(l, 0, 0, Qt.AlignLeft)
|
||||
icon_label = QLabel("Visual reaction test")
|
||||
icon_label.setPixmap(QPixmap(":/icons/visual_task"))
|
||||
grid.addWidget(icon_label, 0, 0, Qt.AlignLeft)
|
||||
|
||||
l2 = QLabel("Measurement of visual reaction times\npress enter to start")
|
||||
heading_label = QLabel("Measurement of visual reaction times\npress enter to start")
|
||||
font = QFont()
|
||||
font.setBold(True)
|
||||
font.setPointSize(20)
|
||||
l2.setFont(font)
|
||||
l2.setStyleSheet("color: #2D4B9A")
|
||||
grid.addWidget(l2, 1, 0, 1, 2, Qt.AlignLeft)
|
||||
heading_label.setFont(font)
|
||||
heading_label.setStyleSheet("color: #2D4B9A")
|
||||
grid.addWidget(heading_label, 1, 0, 1, 2, Qt.AlignLeft)
|
||||
|
||||
settings_btn = QPushButton(cnst.get_icon("settings"), "")
|
||||
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
||||
settings_btn.setToolTip("edit task settings")
|
||||
settings_btn.setShortcut(QKeySequence("alt+s"))
|
||||
settings_btn.clicked.connect(self.on_toggle_settings)
|
||||
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
||||
|
||||
self._status_label = QLabel("Ready to start, press enter ...")
|
||||
grid.addWidget(self._status_label, 3, 0, Qt.AlignBaseline)
|
||||
grid.addWidget(self._status_label, 4, 0, Qt.AlignBaseline)
|
||||
|
||||
self._countdown_label = CountdownLabel(text="Next trial in:")
|
||||
grid.addWidget(self._countdown_label, 3, 1, Qt.AlignCenter)
|
||||
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
||||
self._countdown_label.countdown_done.connect(self.run_trial)
|
||||
|
||||
self._draw_area = QLabel()
|
||||
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
grid.addWidget(self._draw_area, 2, 1)
|
||||
|
||||
self._settings = SettingsPanel()
|
||||
self._settings = VisualTaskSettings()
|
||||
|
||||
self._splitter = QSplitter()
|
||||
self._splitter.addWidget(widget)
|
||||
@ -247,6 +154,7 @@ class VisualBlip(QWidget):
|
||||
return
|
||||
if not self._session_running:
|
||||
self._settings.set_enabled(False)
|
||||
self._settings.store_settings()
|
||||
self._session_running = True
|
||||
self._countdown_label.start(time=self._settings.countdown)
|
||||
|
||||
|
48
blipblop_darwin.spec
Normal file
48
blipblop_darwin.spec
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['blipblop_main.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[('docs/index.md', "docs"),
|
||||
('docs/visual_task.md', "docs"),
|
||||
('docs/auditory_task.md', "docs"),
|
||||
('docs/license.md', "docs"),
|
||||
('docs/tasks.md', "docs"),
|
||||
('docs/images/blipblop_main.png', "docs/images"),
|
||||
('docs/images/blipblop_logo.png', "docs/images"),
|
||||
('sounds/bell.wav', "sounds"),
|
||||
('sounds/complete.wav', "sounds"),
|
||||
('sounds/message.wav', "sounds"),
|
||||
('icons/blipblop_logo.icns', "."),
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='BlipBlop',
|
||||
debug=True,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
icon='icons/blipblop_logo.icns'
|
||||
)
|
51
blipblop_linux.spec
Normal file
51
blipblop_linux.spec
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['blipblop_main.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[('docs/index.md', "docs"),
|
||||
('docs/visual_task.md', "docs"),
|
||||
('docs/auditory_task.md', "docs"),
|
||||
('docs/license.md', "docs"),
|
||||
('docs/results.md', "docs"),
|
||||
('docs/tasks.md', "docs"),
|
||||
('docs/images/blipblop_main.png', "docs/images"),
|
||||
('docs/images/blipblop_logo.png', "docs/images"),
|
||||
('sounds/bell.wav', "sounds"),
|
||||
('sounds/complete.wav', "sounds"),
|
||||
('sounds/message.wav', "sounds"),
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='BlipBlop',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False )
|
||||
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='BlipBlop')
|
49
blipblop_win.spec
Normal file
49
blipblop_win.spec
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['blipblop_main.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[('docs/index.md', "docs"),
|
||||
('docs/visual_task.md', "docs"),
|
||||
('docs/auditory_task.md', "docs"),
|
||||
('docs/license.md', "docs"),
|
||||
('docs/tasks.md', "docs"),
|
||||
('docs/images/blipblop_main.png', "docs/images"),
|
||||
('docs/images/blipblop_logo.png', "docs/images"),
|
||||
('sounds/bell.wav', "sounds"),
|
||||
('sounds/complete.wav', "sounds"),
|
||||
('sounds/message.wav', "sounds"),
|
||||
('icons/blipblop_logo.ico', '.')
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='BlipBlop',
|
||||
debug=True,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
icon='icons/blipblop_logo.ico'
|
||||
)
|
||||
|
2
docs/auditory_task.md
Normal file
2
docs/auditory_task.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Auditory reaction times
|
||||
|
BIN
docs/images/blipblop_logo.png
Normal file
BIN
docs/images/blipblop_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/images/blipblop_main.png
Normal file
BIN
docs/images/blipblop_main.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
@ -1,4 +1,27 @@
|
||||
|
||||
# BlipBlop
|
||||
|
||||
Tiny tool for measuring reaction times upon auditory or visual stimuli.
|
||||

|
||||
|
||||
BlipBlop is a tiny tool for measuring reaction times upon auditory or visual stimulation. This tool is open-source. The source code can be found on [GitHub](https://github.com/jgrewe/blipblop). It is published under the MIT open source [license](license.md).
|
||||
Auditory stimuli have been taken from the freedesktop system sounds [freedesktop](https://freedesktop.org)
|
||||
|
||||
|
||||
## Main Window
|
||||
|
||||

|
||||
|
||||
|
||||
### Visual task
|
||||
|
||||
Measure reaction times to a visual stimulus that pops in the center of the screen. ([more](visual_task.md))
|
||||
|
||||
### Auditory task
|
||||
|
||||
Measure reaction times to a auditory stimulus. ([more](auditory_task.md))
|
||||
|
||||
### Results View
|
||||
|
||||
A simple tablular view that shows the measured reaction times. The data is given in seconds. Select the data and press (**ctrl+c** or **cmd+c**) to copy the selection to clipboard and paste it into your favorite spreadsheet tool for further analysis.
|
||||
|
||||
|
||||
|
9
docs/license.md
Normal file
9
docs/license.md
Normal file
@ -0,0 +1,9 @@
|
||||
# License
|
||||
|
||||
Copyright 2021, Jan Grewe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
1
docs/results.md
Normal file
1
docs/results.md
Normal file
@ -0,0 +1 @@
|
||||
# Results view
|
0
docs/tasks.md
Normal file
0
docs/tasks.md
Normal file
2
docs/visual_task.md
Normal file
2
docs/visual_task.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Visual reaction times
|
||||
|
@ -2,6 +2,7 @@
|
||||
<RCC version="1.0">
|
||||
<qresource prefix="icons">
|
||||
<file alias="app_icon">icons/blipblop_logo.icns</file>
|
||||
<file alias="app_icon_png">icons/blipblop_logo.png</file>
|
||||
<file alias="blipblop_logo">icons/blipblop_logo.png</file>
|
||||
<file alias="visual_task">icons/visual_task.png</file>
|
||||
<file alias="visual_task_large">icons/visual_task_large.png</file>
|
||||
@ -11,17 +12,16 @@
|
||||
<file alias="docs_back">icons/back_btn.png</file>
|
||||
<file alias="docs_forward">icons/fwd_btn.png</file>
|
||||
<file alias="help">icons/help.png</file>
|
||||
<file alias="quit">icons/blipblop_quit.png</file>
|
||||
<file alias="new_session">icons/new_session.png</file>
|
||||
<file alias="new_session_larg">icons/new_session_large.png</file>
|
||||
<file alias="results_table">icons/blipblop_table.png</file>
|
||||
<file alias="settings">icons/settings.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="sounds">
|
||||
<file alias="bell">sounds/bell.wav</file>
|
||||
<file alias="message">sounds/message.wav</file>
|
||||
<file alias="complete">sounds/complete.wav</file>
|
||||
</qresource>
|
||||
<qresource prefix="docs">
|
||||
<file alias="index">docs/index.md</file>
|
||||
<file alias="visual_task">docs/visual_task.md</file>
|
||||
<file alias="auditory_task">docs/auditory_task.md</file>
|
||||
<file alias="license">docs/license.md</file>
|
||||
</qresource>
|
||||
</RCC>
|
Loading…
Reference in New Issue
Block a user