diff --git a/blipblop/ui/audioblop.py b/blipblop/ui/audioblop.py index 1c729ab..95dcb9a 100644 --- a/blipblop/ui/audioblop.py +++ b/blipblop/ui/audioblop.py @@ -5,6 +5,7 @@ from PyQt5.QtMultimedia import QMediaPlayer import os import blipblop.constants as cnst +from blipblop.ui.countdownlabel import CountdownLabel import datetime as dt class SettingsPanel(QWidget): @@ -14,7 +15,8 @@ class SettingsPanel(QWidget): self._trial_spinner = QSpinBox() self._trial_spinner.setMinimum(5) self._trial_spinner.setMaximum(25) - self._trial_spinner.setValue(3) + 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) @@ -28,11 +30,11 @@ class SettingsPanel(QWidget): self._max_delay_spinner.setValue(5) self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]") - self._pause_spinner = QSpinBox() - self._pause_spinner.setMinimum(1) - self._pause_spinner.setMaximum(10) - self._pause_spinner.setValue(3) - self._pause_spinner.setToolTip("Pause between trials [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) @@ -40,7 +42,7 @@ class SettingsPanel(QWidget): 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._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its loudness") self._sound_combo = QComboBox() for k in cnst.SNDS_DICT.keys(): @@ -56,7 +58,7 @@ class SettingsPanel(QWidget): 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._pause_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) @@ -83,8 +85,8 @@ class SettingsPanel(QWidget): return self._max_delay_spinner.value() @property - def pause(self): - return self._pause_spinner.value() + def countdown(self): + return self._countdown_spinner.value() @property def sound(self): @@ -93,7 +95,7 @@ class SettingsPanel(QWidget): def set_enabled(self, enabled): self._trial_spinner.setEnabled(enabled) self._saliency_slider.setEnabled(enabled) - self._pause_spinner.setEnabled(enabled) + self._countdown_spinner.setEnabled(enabled) self._min_delay_spinner.setEnabled(enabled) self._max_delay_spinner.setEnabled(enabled) self._sound_combo.setEnabled(False) @@ -135,6 +137,10 @@ class AudioBlop(QWidget): self._status_label = QLabel("Ready to start, press enter ...") grid.addWidget(self._status_label, 3, 0, Qt.AlignLeft) + self._countdown_label = CountdownLabel(text="Next trial in:") + grid.addWidget(self._countdown_label, 3, 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) @@ -186,15 +192,20 @@ class AudioBlop(QWidget): self._response_time = dt.datetime.now() if self._trial_counter < self._settings.trials: - self._status_label.setText("Trial %i of %i, press enter for next trial" % (self._trial_counter, self._settings.trials)) + self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials)) if self._start_time is None: self._reaction_times.append(-1000) else: reaction_time = self._response_time - self._start_time self._reaction_times.append(reaction_time.total_seconds()) self._trial_running = False + if self._timer.isActive(): + self._timer.stop() if self._trial_counter >= self._settings.trials: self.task_done.emit() + return + self._countdown_label.start(self._settings.countdown) + def reset_canvas(self): bkg_color = QColor() @@ -230,6 +241,9 @@ class AudioBlop(QWidget): if not self._session_running: self._settings.set_enabled(False) self._session_running = True + self._countdown_label.start(time=self._settings.countdown) + + def run_trial(self): self._trial_running = True if self._trial_counter >= self._settings.trials: self.task_done.emit @@ -244,11 +258,11 @@ class AudioBlop(QWidget): max_interval = int(self._settings.max_delay * 10) interval = self._random_generator.bounded(min_interval, max_interval) * 100 self._start_time = None - timer = QTimer(self) - timer.setSingleShot(True) - timer.setInterval(int(interval)) - timer.timeout.connect(self.blip) - timer.start() + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.setInterval(int(interval)) + self._timer.timeout.connect(self.blip) + self._timer.start() def on_abort(self): self.reset() @@ -266,6 +280,7 @@ class AudioBlop(QWidget): self._session_running = False self._status_label.setText("Ready to start...") self._settings.set_enabled(True) + self._countdown_label.stop() def on_toggle_settings(self): if self._splitter.sizes()[1] > 0: diff --git a/blipblop/ui/countdownlabel.py b/blipblop/ui/countdownlabel.py new file mode 100644 index 0000000..6ee26b6 --- /dev/null +++ b/blipblop/ui/countdownlabel.py @@ -0,0 +1,42 @@ +from PyQt5.QtWidgets import QLabel +from PyQt5.QtCore import QTimer, pyqtSignal +from PyQt5.QtGui import QFont + +class CountdownLabel(QLabel): + countdown_done = pyqtSignal() + + def __init__(self, parent=None, text=""): + super().__init__(parent=parent) + self._text = text + self._count = 0 + self._timer = QTimer() + self._timer.timeout.connect(self.on_timeout) + font = QFont() + font.setBold(True) + font.setPointSize(20) + self.setFont(font) + self.setStyleSheet("color: #2D4B9A") + + def start(self, time=3): + if time < 1: + time = 1 + self._count = time + self.setText("%s %i" % (self._text, self._count)) + self.update() + self._timer.start(1000) + + def stop(self): + if self._timer.isActive(): + self._timer.stop() + self.setText("") + + def on_timeout(self): + self._count -= 1 + self.setText("%s %i" % (self._text, self._count)) + self.update() + if self._count <= 0: + self._timer.stop() + self.setText("") + self.update() + self.countdown_done.emit() + \ No newline at end of file diff --git a/blipblop/ui/visualblip.py b/blipblop/ui/visualblip.py index 745e5ca..0608dc7 100644 --- a/blipblop/ui/visualblip.py +++ b/blipblop/ui/visualblip.py @@ -4,6 +4,7 @@ from PyQt5.QtGui import QColor, QFont, QKeySequence, QPainter, QBrush, QPen, QPi import os import blipblop.constants as cnst +from blipblop.ui.countdownlabel import CountdownLabel import datetime as dt class SettingsPanel(QWidget): @@ -13,17 +14,20 @@ class SettingsPanel(QWidget): self._trial_spinner = QSpinBox() self._trial_spinner.setMinimum(5) self._trial_spinner.setMaximum(25) - self._trial_spinner.setValue(3) + self._trial_spinner.setValue(5) + self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)") - self._min_delay_edit = QLineEdit() - self._min_delay_edit.setText(str("1")) - self._min_delay_edit.setToolTip("Minimum delay between start of trial and stimulus display [s]") - self._min_delay_edit.setEnabled(False) + 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_edit = QLineEdit() - self._max_delay_edit.setText(str("5")) - self._max_delay_edit.setToolTip("Maximum delay between start of trial and stimulus display [s]") - self._max_delay_edit.setEnabled(False) + 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) @@ -40,7 +44,13 @@ class SettingsPanel(QWidget): 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) @@ -49,8 +59,9 @@ class SettingsPanel(QWidget): 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_edit) - form_layout.addRow("maximum delay [s]", self._max_delay_edit) + 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) @@ -70,15 +81,23 @@ class SettingsPanel(QWidget): @property def min_delay(self): - return int(self._min_delay_edit.text()) + return self._min_delay_spinner.value() @property def max_delay(self): - return int(self._max_delay_edit.text()) + 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): @@ -100,7 +119,7 @@ class VisualBlip(QWidget): l.setPixmap(QPixmap(os.path.join(cnst.ICONS_FOLDER, "visual_task.png"))) grid.addWidget(l, 0, 0, Qt.AlignLeft) - l2 =QLabel("Measurement of visual reaction times\npress enter to start") + l2 = QLabel("Measurement of visual reaction times\npress enter to start") font = QFont() font.setBold(True) font.setPointSize(20) @@ -116,7 +135,11 @@ class VisualBlip(QWidget): self._status_label = QLabel("Ready to start, press enter ...") grid.addWidget(self._status_label, 3, 0, Qt.AlignBaseline) - + + self._countdown_label = CountdownLabel(text="Next trial in:") + grid.addWidget(self._countdown_label, 3, 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) @@ -168,7 +191,7 @@ class VisualBlip(QWidget): self._response_time = dt.datetime.now() if self._trial_counter < self._settings.trials: - self._status_label.setText("Trial %i of %i, press enter for next trial" % (self._trial_counter, self._settings.trials)) + self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials)) if self._start_time is None: self._reaction_times.append(-1000) else: @@ -176,8 +199,12 @@ class VisualBlip(QWidget): self._reaction_times.append(reaction_time.total_seconds()) self.reset_canvas() self._trial_running = False + if self._timer.isActive(): + self._timer.stop() if self._trial_counter >= self._settings.trials: self.task_done.emit() + return + self._countdown_label.start(self._settings.countdown) def reset_canvas(self): bkg_color = QColor() @@ -220,7 +247,10 @@ class VisualBlip(QWidget): return if not self._session_running: self._settings.set_enabled(False) - self._session_running = True + self._session_running = True + self._countdown_label.start(time=self._settings.countdown) + + def run_trial(self): self._trial_running = True if self._trial_counter >= self._settings.trials: self.task_done.emit @@ -232,11 +262,11 @@ class VisualBlip(QWidget): max_interval = int(self._settings.max_delay * 10) interval = self._random_generator.bounded(min_interval, max_interval) * 100 self._start_time = None - timer = QTimer(self) - timer.setSingleShot(True) - timer.setInterval(int(interval)) - timer.timeout.connect(self.blip) - timer.start() + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.setInterval(int(interval)) + self._timer.timeout.connect(self.blip) + self._timer.start() def on_abort(self): self.reset() @@ -254,6 +284,7 @@ class VisualBlip(QWidget): self._trial_running = False self._session_running = False self._status_label.setText("Ready to start...") + self._countdown_label.stop() self._settings.set_enabled(True) def on_toggle_settings(self):