[settings] extract tasksettings class
This commit is contained in:
parent
4695d60cd1
commit
2b5982eabb
@ -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, endl, pyqtSignal
|
from PyQt5.QtCore import QPoint, QRandomGenerator, QTimer, Qt, pyqtSignal
|
||||||
from PyQt5.QtGui import QColor, QFont, QIcon, QKeySequence, QPainter, QPen, QPixmap
|
from PyQt5.QtGui import QColor, QFont, QIcon, QKeySequence, QPainter, QPen, QPixmap
|
||||||
from PyQt5.QtMultimedia import QMediaPlayer
|
from PyQt5.QtMultimedia import QMediaPlayer
|
||||||
|
|
||||||
import os
|
|
||||||
import blipblop.constants as cnst
|
import blipblop.constants as cnst
|
||||||
from blipblop.ui.countdownlabel import CountdownLabel
|
from blipblop.ui.countdownlabel import CountdownLabel
|
||||||
|
from blipblop.ui.settings import AuditoryTaskSettings
|
||||||
import datetime as dt
|
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(enabled)
|
|
||||||
|
|
||||||
|
|
||||||
class AudioBlop(QWidget):
|
class AudioBlop(QWidget):
|
||||||
task_done = pyqtSignal()
|
task_done = pyqtSignal()
|
||||||
@ -107,7 +15,7 @@ class AudioBlop(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
grid.setColumnStretch(0, 1)
|
grid.setColumnStretch(0, 1)
|
||||||
@ -116,37 +24,37 @@ class AudioBlop(QWidget):
|
|||||||
grid.setRowStretch(3, 1)
|
grid.setRowStretch(3, 1)
|
||||||
widget.setLayout(grid)
|
widget.setLayout(grid)
|
||||||
|
|
||||||
l = QLabel("Auditory reaction test")
|
icon_label = QLabel("Auditory reaction test")
|
||||||
l.setPixmap(QPixmap(":/icons/auditory_task"))
|
icon_label.setPixmap(QPixmap(":/icons/auditory_task"))
|
||||||
grid.addWidget(l, 0, 0, Qt.AlignLeft)
|
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 = QFont()
|
||||||
font.setBold(True)
|
font.setBold(True)
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
l2.setFont(font)
|
heading_label.setFont(font)
|
||||||
l2.setStyleSheet("color: #2D4B9A")
|
heading_label.setStyleSheet("color: #2D4B9A")
|
||||||
grid.addWidget(l2, 1, 0, 1, 2, Qt.AlignLeft)
|
grid.addWidget(heading_label, 1, 0, 1, 2, Qt.AlignLeft)
|
||||||
|
|
||||||
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
||||||
settings_btn.setToolTip("edit task settings")
|
settings_btn.setToolTip("edit task settings")
|
||||||
settings_btn.setShortcut(QKeySequence("alt+s"))
|
settings_btn.setShortcut(QKeySequence("alt+s"))
|
||||||
settings_btn.clicked.connect(self.on_toggle_settings)
|
settings_btn.clicked.connect(self.on_toggle_settings)
|
||||||
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
||||||
|
|
||||||
self._status_label = QLabel("Ready to start, press enter ...")
|
self._status_label = QLabel("Ready to start, press enter ...")
|
||||||
grid.addWidget(self._status_label, 4, 0, Qt.AlignLeft)
|
grid.addWidget(self._status_label, 4, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
self._countdown_label = CountdownLabel(text="Next trial in:")
|
self._countdown_label = CountdownLabel(text="Next trial in:")
|
||||||
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
||||||
self._countdown_label.countdown_done.connect(self.run_trial)
|
self._countdown_label.countdown_done.connect(self.run_trial)
|
||||||
|
|
||||||
self._draw_area = QLabel()
|
self._draw_area = QLabel()
|
||||||
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
grid.addWidget(self._draw_area, 2, 1)
|
grid.addWidget(self._draw_area, 2, 1)
|
||||||
|
|
||||||
self._settings = SettingsPanel()
|
self._settings = AuditoryTaskSettings()
|
||||||
|
|
||||||
self._splitter = QSplitter()
|
self._splitter = QSplitter()
|
||||||
self._splitter.addWidget(widget)
|
self._splitter.addWidget(widget)
|
||||||
self._splitter.addWidget(self._settings)
|
self._splitter.addWidget(self._settings)
|
||||||
@ -155,7 +63,7 @@ class AudioBlop(QWidget):
|
|||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(self._splitter)
|
vbox.addWidget(self._splitter)
|
||||||
self.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
|
|
||||||
self.reset_canvas()
|
self.reset_canvas()
|
||||||
self.create_actions()
|
self.create_actions()
|
||||||
|
|
||||||
@ -168,12 +76,12 @@ class AudioBlop(QWidget):
|
|||||||
self._player = QMediaPlayer()
|
self._player = QMediaPlayer()
|
||||||
self._random_generator = QRandomGenerator()
|
self._random_generator = QRandomGenerator()
|
||||||
self.setFocus()
|
self.setFocus()
|
||||||
|
|
||||||
def create_actions(self):
|
def create_actions(self):
|
||||||
self._start_action = QAction("start trial")
|
self._start_action = QAction("start trial")
|
||||||
self._start_action.setShortcuts([QKeySequence("enter"), QKeySequence("return")])
|
self._start_action.setShortcuts([QKeySequence("enter"), QKeySequence("return")])
|
||||||
self._start_action.triggered.connect(self.on_trial_start)
|
self._start_action.triggered.connect(self.on_trial_start)
|
||||||
|
|
||||||
self._reaction = QAction("reaction")
|
self._reaction = QAction("reaction")
|
||||||
self._reaction.setShortcut(QKeySequence("space"))
|
self._reaction.setShortcut(QKeySequence("space"))
|
||||||
self._reaction.triggered.connect(self.on_reaction)
|
self._reaction.triggered.connect(self.on_reaction)
|
||||||
@ -181,7 +89,7 @@ class AudioBlop(QWidget):
|
|||||||
self._abort = QAction("abort")
|
self._abort = QAction("abort")
|
||||||
self._abort.setShortcut(QKeySequence("escape"))
|
self._abort.setShortcut(QKeySequence("escape"))
|
||||||
self._abort.triggered.connect(self.on_abort)
|
self._abort.triggered.connect(self.on_abort)
|
||||||
|
|
||||||
self.addAction(self._start_action)
|
self.addAction(self._start_action)
|
||||||
self.addAction(self._reaction)
|
self.addAction(self._reaction)
|
||||||
self.addAction(self._abort)
|
self.addAction(self._abort)
|
||||||
@ -189,7 +97,7 @@ class AudioBlop(QWidget):
|
|||||||
def on_reaction(self):
|
def on_reaction(self):
|
||||||
if not self._session_running or not self._trial_running:
|
if not self._session_running or not self._trial_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._response_time = dt.datetime.now()
|
self._response_time = dt.datetime.now()
|
||||||
if self._trial_counter < self._settings.trials:
|
if self._trial_counter < self._settings.trials:
|
||||||
self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
|
self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
|
||||||
@ -206,14 +114,13 @@ class AudioBlop(QWidget):
|
|||||||
return
|
return
|
||||||
self._countdown_label.start(self._settings.countdown)
|
self._countdown_label.start(self._settings.countdown)
|
||||||
|
|
||||||
|
|
||||||
def reset_canvas(self):
|
def reset_canvas(self):
|
||||||
bkg_color = QColor()
|
bkg_color = QColor()
|
||||||
bkg_color.setAlphaF(0.0)
|
bkg_color.setAlphaF(0.0)
|
||||||
canvas = QPixmap(400, 400)
|
canvas = QPixmap(400, 400)
|
||||||
self._canvas_center = QPoint(200, 200)
|
self._canvas_center = QPoint(200, 200)
|
||||||
canvas.fill(bkg_color)
|
canvas.fill(bkg_color)
|
||||||
self.draw_fixation(canvas)
|
self.draw_fixation(canvas)
|
||||||
self._draw_area.setPixmap(canvas)
|
self._draw_area.setPixmap(canvas)
|
||||||
self._draw_area.update()
|
self._draw_area.update()
|
||||||
|
|
||||||
@ -222,7 +129,7 @@ class AudioBlop(QWidget):
|
|||||||
right = QPoint(225, 200)
|
right = QPoint(225, 200)
|
||||||
top = QPoint(200, 175)
|
top = QPoint(200, 175)
|
||||||
bottom = QPoint(200, 225)
|
bottom = QPoint(200, 225)
|
||||||
painter = QPainter(pixmap)
|
painter = QPainter(pixmap)
|
||||||
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
|
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
|
||||||
painter.drawLine(left, right)
|
painter.drawLine(left, right)
|
||||||
painter.drawLine(top, bottom)
|
painter.drawLine(top, bottom)
|
||||||
@ -234,7 +141,7 @@ class AudioBlop(QWidget):
|
|||||||
def blip(self):
|
def blip(self):
|
||||||
self._player.play()
|
self._player.play()
|
||||||
self._start_time = dt.datetime.now()
|
self._start_time = dt.datetime.now()
|
||||||
|
|
||||||
def on_trial_start(self):
|
def on_trial_start(self):
|
||||||
if self._trial_running:
|
if self._trial_running:
|
||||||
return
|
return
|
||||||
@ -251,9 +158,10 @@ class AudioBlop(QWidget):
|
|||||||
self._trial_counter += 1
|
self._trial_counter += 1
|
||||||
self._status_label.setText("Trial %i of %i running" % (self._trial_counter, self._settings.trials))
|
self._status_label.setText("Trial %i of %i running" % (self._trial_counter, self._settings.trials))
|
||||||
content = cnst.get_sound(self._settings.sound)
|
content = cnst.get_sound(self._settings.sound)
|
||||||
|
|
||||||
self._player.setMedia(content)
|
self._player.setMedia(content)
|
||||||
self._player.setVolume(self._settings.saliency)
|
self._player.setVolume(self._settings.saliency)
|
||||||
|
|
||||||
min_interval = int(self._settings.min_delay * 10)
|
min_interval = int(self._settings.min_delay * 10)
|
||||||
max_interval = int(self._settings.max_delay * 10)
|
max_interval = int(self._settings.max_delay * 10)
|
||||||
interval = self._random_generator.bounded(min_interval, max_interval) * 100
|
interval = self._random_generator.bounded(min_interval, max_interval) * 100
|
||||||
@ -263,15 +171,15 @@ class AudioBlop(QWidget):
|
|||||||
self._timer.setInterval(int(interval))
|
self._timer.setInterval(int(interval))
|
||||||
self._timer.timeout.connect(self.blip)
|
self._timer.timeout.connect(self.blip)
|
||||||
self._timer.start()
|
self._timer.start()
|
||||||
|
|
||||||
def on_abort(self):
|
def on_abort(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
self.task_aborted.emit()
|
self.task_aborted.emit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def results(self):
|
def results(self):
|
||||||
return self._reaction_times
|
return self._reaction_times
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._trial_counter = 0
|
self._trial_counter = 0
|
||||||
self._session_running = 0
|
self._session_running = 0
|
||||||
@ -281,7 +189,7 @@ class AudioBlop(QWidget):
|
|||||||
self._status_label.setText("Ready to start...")
|
self._status_label.setText("Ready to start...")
|
||||||
self._settings.set_enabled(True)
|
self._settings.set_enabled(True)
|
||||||
self._countdown_label.stop()
|
self._countdown_label.stop()
|
||||||
|
|
||||||
def on_toggle_settings(self):
|
def on_toggle_settings(self):
|
||||||
if self._splitter.sizes()[1] > 0:
|
if self._splitter.sizes()[1] > 0:
|
||||||
self._splitter.widget(1).hide()
|
self._splitter.widget(1).hide()
|
||||||
|
132
blipblop/ui/settings.py
Normal file
132
blipblop/ui/settings.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
from PyQt5.QtWidgets import QComboBox, QFormLayout, QSlider, QSpinBox, QTextEdit, QWidget
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
import blipblop.constants as cnst
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSettings(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._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)
|
||||||
|
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
|
||||||
|
@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(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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
return self._size_slider.sliderPosition()
|
@ -1,104 +1,11 @@
|
|||||||
from PyQt5.QtWidgets import QAction, QFormLayout, QGridLayout, QLabel, QLineEdit, 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, QSettings
|
from PyQt5.QtCore import QPoint, QRandomGenerator, QTimer, Qt, pyqtSignal
|
||||||
from PyQt5.QtGui import QColor, QFont, QIcon, QKeySequence, QPainter, QBrush, QPen, QPixmap
|
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.countdownlabel import CountdownLabel
|
||||||
|
from blipblop.ui.settings import VisualTaskSettings
|
||||||
import datetime as dt
|
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):
|
class VisualBlip(QWidget):
|
||||||
task_done = pyqtSignal()
|
task_done = pyqtSignal()
|
||||||
@ -106,7 +13,7 @@ class VisualBlip(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
grid.setColumnStretch(0, 1)
|
grid.setColumnStretch(0, 1)
|
||||||
@ -115,37 +22,37 @@ class VisualBlip(QWidget):
|
|||||||
grid.setRowStretch(3, 1)
|
grid.setRowStretch(3, 1)
|
||||||
widget.setLayout(grid)
|
widget.setLayout(grid)
|
||||||
|
|
||||||
l = QLabel("Visual reaction test")
|
icon_label = QLabel("Visual reaction test")
|
||||||
l.setPixmap(QPixmap(":/icons/visual_task"))
|
icon_label.setPixmap(QPixmap(":/icons/visual_task"))
|
||||||
grid.addWidget(l, 0, 0, Qt.AlignLeft)
|
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 = QFont()
|
||||||
font.setBold(True)
|
font.setBold(True)
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
l2.setFont(font)
|
heading_label.setFont(font)
|
||||||
l2.setStyleSheet("color: #2D4B9A")
|
heading_label.setStyleSheet("color: #2D4B9A")
|
||||||
grid.addWidget(l2, 1, 0, 1, 2, Qt.AlignLeft)
|
grid.addWidget(heading_label, 1, 0, 1, 2, Qt.AlignLeft)
|
||||||
|
|
||||||
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
settings_btn = QPushButton(QIcon(":/icons/settings"), "")
|
||||||
settings_btn.setToolTip("edit task settings")
|
settings_btn.setToolTip("edit task settings")
|
||||||
settings_btn.setShortcut(QKeySequence("alt+s"))
|
settings_btn.setShortcut(QKeySequence("alt+s"))
|
||||||
settings_btn.clicked.connect(self.on_toggle_settings)
|
settings_btn.clicked.connect(self.on_toggle_settings)
|
||||||
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
grid.addWidget(settings_btn, 0, 3, Qt.AlignRight)
|
||||||
|
|
||||||
self._status_label = QLabel("Ready to start, press enter ...")
|
self._status_label = QLabel("Ready to start, press enter ...")
|
||||||
grid.addWidget(self._status_label, 4, 0, Qt.AlignBaseline)
|
grid.addWidget(self._status_label, 4, 0, Qt.AlignBaseline)
|
||||||
|
|
||||||
self._countdown_label = CountdownLabel(text="Next trial in:")
|
self._countdown_label = CountdownLabel(text="Next trial in:")
|
||||||
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
grid.addWidget(self._countdown_label, 4, 1, Qt.AlignCenter)
|
||||||
self._countdown_label.countdown_done.connect(self.run_trial)
|
self._countdown_label.countdown_done.connect(self.run_trial)
|
||||||
|
|
||||||
self._draw_area = QLabel()
|
self._draw_area = QLabel()
|
||||||
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
grid.addWidget(self._draw_area, 2, 1)
|
grid.addWidget(self._draw_area, 2, 1)
|
||||||
|
|
||||||
self._settings = SettingsPanel()
|
self._settings = VisualTaskSettings()
|
||||||
|
|
||||||
self._splitter = QSplitter()
|
self._splitter = QSplitter()
|
||||||
self._splitter.addWidget(widget)
|
self._splitter.addWidget(widget)
|
||||||
self._splitter.addWidget(self._settings)
|
self._splitter.addWidget(self._settings)
|
||||||
@ -154,7 +61,7 @@ class VisualBlip(QWidget):
|
|||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(self._splitter)
|
vbox.addWidget(self._splitter)
|
||||||
self.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
|
|
||||||
self.reset_canvas()
|
self.reset_canvas()
|
||||||
self.create_actions()
|
self.create_actions()
|
||||||
|
|
||||||
@ -167,12 +74,12 @@ class VisualBlip(QWidget):
|
|||||||
self._random_generator = QRandomGenerator()
|
self._random_generator = QRandomGenerator()
|
||||||
|
|
||||||
self.setFocus()
|
self.setFocus()
|
||||||
|
|
||||||
def create_actions(self):
|
def create_actions(self):
|
||||||
self._start_action = QAction("start trial")
|
self._start_action = QAction("start trial")
|
||||||
self._start_action.setShortcuts([QKeySequence("enter"), QKeySequence("return")])
|
self._start_action.setShortcuts([QKeySequence("enter"), QKeySequence("return")])
|
||||||
self._start_action.triggered.connect(self.on_trial_start)
|
self._start_action.triggered.connect(self.on_trial_start)
|
||||||
|
|
||||||
self._reaction = QAction("reaction")
|
self._reaction = QAction("reaction")
|
||||||
self._reaction.setShortcut(QKeySequence("space"))
|
self._reaction.setShortcut(QKeySequence("space"))
|
||||||
self._reaction.triggered.connect(self.on_reaction)
|
self._reaction.triggered.connect(self.on_reaction)
|
||||||
@ -180,7 +87,7 @@ class VisualBlip(QWidget):
|
|||||||
self._abort = QAction("abort")
|
self._abort = QAction("abort")
|
||||||
self._abort.setShortcut(QKeySequence("escape"))
|
self._abort.setShortcut(QKeySequence("escape"))
|
||||||
self._abort.triggered.connect(self.on_abort)
|
self._abort.triggered.connect(self.on_abort)
|
||||||
|
|
||||||
self.addAction(self._start_action)
|
self.addAction(self._start_action)
|
||||||
self.addAction(self._reaction)
|
self.addAction(self._reaction)
|
||||||
self.addAction(self._abort)
|
self.addAction(self._abort)
|
||||||
@ -188,7 +95,7 @@ class VisualBlip(QWidget):
|
|||||||
def on_reaction(self):
|
def on_reaction(self):
|
||||||
if not self._session_running or not self._trial_running:
|
if not self._session_running or not self._trial_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._response_time = dt.datetime.now()
|
self._response_time = dt.datetime.now()
|
||||||
if self._trial_counter < self._settings.trials:
|
if self._trial_counter < self._settings.trials:
|
||||||
self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
|
self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
|
||||||
@ -212,7 +119,7 @@ class VisualBlip(QWidget):
|
|||||||
canvas = QPixmap(400, 400)
|
canvas = QPixmap(400, 400)
|
||||||
self._canvas_center = QPoint(200, 200)
|
self._canvas_center = QPoint(200, 200)
|
||||||
canvas.fill(bkg_color)
|
canvas.fill(bkg_color)
|
||||||
self.draw_fixation(canvas)
|
self.draw_fixation(canvas)
|
||||||
self._draw_area.setPixmap(canvas)
|
self._draw_area.setPixmap(canvas)
|
||||||
self._draw_area.update()
|
self._draw_area.update()
|
||||||
|
|
||||||
@ -221,7 +128,7 @@ class VisualBlip(QWidget):
|
|||||||
right = QPoint(225, 200)
|
right = QPoint(225, 200)
|
||||||
top = QPoint(200, 175)
|
top = QPoint(200, 175)
|
||||||
bottom = QPoint(200, 225)
|
bottom = QPoint(200, 225)
|
||||||
painter = QPainter(pixmap)
|
painter = QPainter(pixmap)
|
||||||
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
|
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
|
||||||
painter.drawLine(left, right)
|
painter.drawLine(left, right)
|
||||||
painter.drawLine(top, bottom)
|
painter.drawLine(top, bottom)
|
||||||
@ -232,24 +139,24 @@ class VisualBlip(QWidget):
|
|||||||
|
|
||||||
def blip(self):
|
def blip(self):
|
||||||
stim_size = self._settings.size
|
stim_size = self._settings.size
|
||||||
painter = QPainter(self._draw_area.pixmap())
|
painter = QPainter(self._draw_area.pixmap())
|
||||||
painter.setPen(QPen(Qt.red, 1, Qt.SolidLine))
|
painter.setPen(QPen(Qt.red, 1, Qt.SolidLine))
|
||||||
color = QColor(Qt.red)
|
color = QColor(Qt.red)
|
||||||
color.setAlphaF(self._settings.saliency/100)
|
color.setAlphaF(self._settings.saliency/100)
|
||||||
painter.setBrush(QBrush(color, Qt.SolidPattern))
|
painter.setBrush(QBrush(color, Qt.SolidPattern))
|
||||||
painter.drawEllipse(self._canvas_center, stim_size, stim_size)
|
painter.drawEllipse(self._canvas_center, stim_size, stim_size)
|
||||||
painter.end()
|
painter.end()
|
||||||
self._start_time = dt.datetime.now()
|
self._start_time = dt.datetime.now()
|
||||||
self._draw_area.update()
|
self._draw_area.update()
|
||||||
|
|
||||||
def on_trial_start(self):
|
def on_trial_start(self):
|
||||||
if self._trial_running:
|
if self._trial_running:
|
||||||
return
|
return
|
||||||
if not self._session_running:
|
if not self._session_running:
|
||||||
self._settings.set_enabled(False)
|
self._settings.set_enabled(False)
|
||||||
self._session_running = True
|
self._session_running = True
|
||||||
self._countdown_label.start(time=self._settings.countdown)
|
self._countdown_label.start(time=self._settings.countdown)
|
||||||
|
|
||||||
def run_trial(self):
|
def run_trial(self):
|
||||||
self._trial_running = True
|
self._trial_running = True
|
||||||
if self._trial_counter >= self._settings.trials:
|
if self._trial_counter >= self._settings.trials:
|
||||||
@ -267,15 +174,15 @@ class VisualBlip(QWidget):
|
|||||||
self._timer.setInterval(int(interval))
|
self._timer.setInterval(int(interval))
|
||||||
self._timer.timeout.connect(self.blip)
|
self._timer.timeout.connect(self.blip)
|
||||||
self._timer.start()
|
self._timer.start()
|
||||||
|
|
||||||
def on_abort(self):
|
def on_abort(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
self.task_aborted.emit()
|
self.task_aborted.emit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def results(self):
|
def results(self):
|
||||||
return self._reaction_times
|
return self._reaction_times
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.reset_canvas()
|
self.reset_canvas()
|
||||||
self._trial_counter = 0
|
self._trial_counter = 0
|
||||||
@ -286,7 +193,7 @@ class VisualBlip(QWidget):
|
|||||||
self._status_label.setText("Ready to start...")
|
self._status_label.setText("Ready to start...")
|
||||||
self._countdown_label.stop()
|
self._countdown_label.stop()
|
||||||
self._settings.set_enabled(True)
|
self._settings.set_enabled(True)
|
||||||
|
|
||||||
def on_toggle_settings(self):
|
def on_toggle_settings(self):
|
||||||
if self._splitter.sizes()[1] > 0:
|
if self._splitter.sizes()[1] > 0:
|
||||||
self._splitter.widget(1).hide()
|
self._splitter.widget(1).hide()
|
||||||
|
Loading…
Reference in New Issue
Block a user