Source code for wonambi.widgets.notes

# -*- coding: utf-8 -*-

"""Widgets containing notes (such as markers, events, and stages).

  - bookmarks are unique (might have the same text), are not mutually
    exclusive, have variable duration
  - events are not unique, are not mutually exclusive, have variable duration
  - stages are not unique, are mutually exclusive, have fixed duration


TODO:
    maybe it's better to disable options related to annotations, but it's a bit
    too complicated. If you do that, you can remove all "if self.annot is None"
    that are marked with "# remove if buttons are disabled"
"""
from datetime import datetime, timedelta
from functools import partial
from logging import getLogger
from numpy import asarray, floor
from os.path import basename, splitext
from pathlib import Path

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QColor
from PyQt5.QtWidgets import (QAbstractItemView,
                             QAction,
                             QCheckBox,
                             QComboBox,
                             QDialog,
                             QDialogButtonBox,
                             QErrorMessage,
                             QFileDialog,
                             QFormLayout,
                             QGroupBox,
                             QHBoxLayout,
                             QInputDialog,
                             QLabel,
                             QLineEdit,
                             QListWidget,
                             QListWidgetItem,
                             QMessageBox,
                             QProgressDialog,
                             QPushButton,
                             QSpinBox,
                             QTableWidget,
                             QTableWidgetItem,
                             QTabWidget,
                             QVBoxLayout,
                             QWidget,
                             QScrollArea,
                             )

from .. import ChanTime
from ..attr import Annotations, create_empty_annotations
from ..attr.annotations import create_annotation
from ..detect import DetectSpindle, DetectSlowWave, merge_close
from ..ioeeg import _write_vmrk
from .settings import Config
from .utils import (convert_name_to_color, FormStr, FormInt, FormFloat,
                    FormBool, FormMenu, short_strings, ICON, STAGE_NAME)
from .modal_widgets import DateTimeDialog

lg = getLogger(__name__)

MAX_FREQUENCY_OF_INTEREST = 50

STAGE_SHORTCUT = ['1', '2', '3', '5', '9', '8', '0', '', '', '7']
QUALIFIERS = ['Good', 'Poor']
QUALITY_SHORTCUT = ['o', 'p']
SPINDLE_METHODS = ['Moelle2011',
                   'Lacourse2018',
                   'Ray2015',
                   'Martin2013',
                   'Wamsley2012',
                   'Nir2011',
                   'Ferrarelli2007',
                   'FASST',
                   'FASST2',
                   'UCSD',
                   'Concordia',
                   ]
SLOW_WAVE_METHODS = ['Massimini2004', 'AASM/Massimini2004', 
                     #'Ngo2015', 
                     #'Staresina2015'
                     ]


[docs]class ConfigNotes(Config): """Widget with preferences in Settings window for the Annotations.""" def __init__(self, update_widget): super().__init__('notes', update_widget)
[docs] def create_config(self): box0 = QGroupBox('Markers') self.index['marker_show'] = FormBool('Display Markers in Dataset') self.index['marker_color'] = FormStr() self.index['annot_show'] = FormBool('Display User-Made Annotations') self.index['annot_bookmark_color'] = FormStr() self.index['min_marker_display_dur'] = FormFloat() self.index['min_marker_dur'] = FormFloat() flayout = QFormLayout() flayout.addRow(self.index['marker_show']) flayout.addRow('Color of markers in the dataset', self.index['marker_color']) flayout.addRow(self.index['annot_show']) flayout.addRow('Color of bookmarks in annotations', self.index['annot_bookmark_color']) flayout.addRow('Event markers appear at least this long on trace', self.index['min_marker_display_dur']) flayout.addRow('Below this duration, markers and events have no ' 'duration', self.index['min_marker_dur']) box0.setLayout(flayout) box1 = QGroupBox('Events') flayout = QFormLayout() box1.setLayout(flayout) box2 = QGroupBox('Stages') self.index['scoring_window'] = FormInt() flayout = QFormLayout() flayout.addRow('Default scoring epoch length', self.index['scoring_window']) box2.setLayout(flayout) main_layout = QVBoxLayout() main_layout.addWidget(box0) main_layout.addWidget(box1) main_layout.addWidget(box2) main_layout.addStretch(1) self.setLayout(main_layout)
[docs]class Notes(QTabWidget): """Widget that contains information about sleep scoring. Attributes ---------- parent : instance of QMainWindow the main window. config : ConfigNotes preferences for this widget annot : Annotations contains the annotations made by the user idx_marker : QTableWidget table with the markers in the dataset idx_summary: QVBoxLayout layout of the "Summary" tab (we add and remove the "Recap" box) idx_annotations : QPushButton push button with the text of the annotation file idx_rater : QLabel name of the current rater idx_stats : QFormLayout layout of the stage statistics idx_eventtype_scroll : QScrollArea area to which you add the QGroupBox with the list of events as checkbox idx_eventtype_list : list of QCheckBox list of checkboxes with the event types idx_annot_list : QTableWidget table with the bookmarks and events in the annotations idx_eventtype : QComboBox Combo box of the event types for the toolbar idx_stage : QComboBox Combo box of the stages for the toolbar idx_quality : QComboBox Combo box of signal quality (good/poor) for the toolbar """ def __init__(self, parent): super().__init__() self.parent = parent self.config = ConfigNotes(self.update_settings) self.annot = None self.idx_marker = None self.idx_summary = None self.idx_annotations = None self.idx_rater = None self.idx_stats = None self.idx_eventtype_scroll = None self.idx_eventtype_list = [] self.idx_annot_list = None self.idx_eventtype = None self.idx_stage = None self.idx_quality = None self.create() self.create_action()
[docs] def create(self): """Create the widget layout with all the annotations.""" """ ------ MARKERS ------ """ tab0 = QTableWidget() self.idx_marker = tab0 tab0.setColumnCount(3) tab0.horizontalHeader().setStretchLastSection(True) tab0.setSelectionBehavior(QAbstractItemView.SelectRows) tab0.setEditTriggers(QAbstractItemView.NoEditTriggers) go_to_marker = lambda r, c: self.go_to_marker(r, c, 'dataset') tab0.cellDoubleClicked.connect(go_to_marker) tab0.setHorizontalHeaderLabels(['Start', 'Duration', 'Text']) """ ------ SUMMARY ------ """ tab1 = QWidget() self.idx_eventtype = QComboBox(self) self.idx_stage = QComboBox(self) self.idx_stage.activated.connect(self.get_sleepstage) self.idx_quality = QComboBox(self) self.idx_quality.activated.connect(self.get_quality) self.idx_annotations = QPushButton('Load Annotation File...') self.idx_annotations.clicked.connect(self.load_annot) self.idx_rater = QLabel('') b0 = QGroupBox('Info') form = QFormLayout() b0.setLayout(form) form.addRow('File:', self.idx_annotations) form.addRow('Rater:', self.idx_rater) b1 = QGroupBox('Staging') b2 = QGroupBox('Signal quality') layout = QVBoxLayout() layout.addWidget(b0) layout.addWidget(b1) layout.addWidget(b2) self.idx_summary = layout tab1.setLayout(layout) """ ------ ANNOTATIONS ------ """ tab2 = QWidget() tab_annot = QTableWidget() self.idx_annot_list = tab_annot delete_row = QPushButton('Delete') delete_row.clicked.connect(self.delete_row) scroll = QScrollArea(tab2) scroll.setWidgetResizable(True) evttype_group = QGroupBox('Event Types') scroll.setWidget(evttype_group) self.idx_eventtype_scroll = scroll tab_annot.setColumnCount(5) tab_annot.setHorizontalHeaderLabels(['Start', 'Duration', 'Text', 'Type', 'Channel']) tab_annot.horizontalHeader().setStretchLastSection(True) tab_annot.setSelectionBehavior(QAbstractItemView.SelectRows) tab_annot.setEditTriggers(QAbstractItemView.NoEditTriggers) go_to_annot = lambda r, c: self.go_to_marker(r, c, 'annot') tab_annot.cellDoubleClicked.connect(go_to_annot) tab_annot.cellDoubleClicked.connect(self.reset_current_row) layout = QVBoxLayout() layout.addWidget(self.idx_eventtype_scroll, stretch=1) layout.addWidget(self.idx_annot_list) layout.addWidget(delete_row) tab2.setLayout(layout) """ ------ TABS ------ """ self.addTab(tab0, 'Markers') self.addTab(tab1, 'Summary') # disable self.addTab(tab2, 'Annotations') # disable
[docs] def create_action(self): """Create actions associated with Annotations.""" actions = {} act = QAction('New Annotations', self) act.triggered.connect(self.new_annot) actions['new_annot'] = act act = QAction('Load Annotations', self) act.triggered.connect(self.load_annot) actions['load_annot'] = act act = QAction('Clear Annotations...', self) act.triggered.connect(self.clear_annot) actions['clear_annot'] = act act = QAction('New...', self) act.triggered.connect(self.new_rater) actions['new_rater'] = act act = QAction('Rename...', self) act.triggered.connect(self.rename_rater) actions['rename_rater'] = act act = QAction('Delete...', self) act.triggered.connect(self.delete_rater) actions['del_rater'] = act act = QAction(QIcon(ICON['bookmark']), 'New Bookmark', self) act.setCheckable(True) actions['new_bookmark'] = act act = QAction(QIcon(ICON['new_eventtype']), 'New Event Type', self) act.triggered.connect(self.new_eventtype) actions['new_eventtype'] = act act = QAction(QIcon(ICON['del_eventtype']), 'Delete Event Type', self) act.triggered.connect(self.delete_eventtype) actions['del_eventtype'] = act act = QAction('Rename Event Type', self) act.triggered.connect(self.rename_eventtype) actions['rename_eventtype'] = act act = QAction('New Name', self) act.triggered.connect(self.markers_to_events) actions['m2e_newname'] = act act = QAction('Keep Marker Names', self) act.triggered.connect(partial(self.markers_to_events, True)) actions['m2e_keepname'] = act act = QAction('Merge Events...', self) act.triggered.connect(self.parent.show_merge_dialog) actions['merge_events'] = act act = QAction(QIcon(ICON['event']), 'Event Mode', self) act.setCheckable(True) actions['new_event'] = act uncheck_new_event = lambda: actions['new_event'].setChecked(False) uncheck_new_bookmark = lambda: actions['new_bookmark'].setChecked(False) actions['new_event'].triggered.connect(uncheck_new_bookmark) actions['new_bookmark'].triggered.connect(uncheck_new_event) act = {} for one_stage, one_shortcut in zip(STAGE_NAME, STAGE_SHORTCUT): act[one_stage] = QAction('Score as ' + one_stage, self.parent) act[one_stage].setShortcut(one_shortcut) stage_idx = STAGE_NAME.index(one_stage) act[one_stage].triggered.connect(partial(self.get_sleepstage, stage_idx)) self.addAction(act[one_stage]) actions['stages'] = act act = {} for one_qual, one_shortcut in zip(QUALIFIERS, QUALITY_SHORTCUT): act[one_qual] = QAction('Score as ' + one_qual, self.parent) act[one_qual].setShortcut(one_shortcut) qual_idx = QUALIFIERS.index(one_qual) act[one_qual].triggered.connect(partial(self.get_quality, qual_idx)) self.addAction(act[one_qual]) actions['quality'] = act act = QAction('Set Cycle Start', self) act.setShortcut('Ctrl+[') act.triggered.connect(self.get_cycle_mrkr) actions['cyc_start'] = act act = QAction('Set Cycle End', self) act.setShortcut('Ctrl+]') act.triggered.connect(partial(self.get_cycle_mrkr, end=True)) actions['cyc_end'] = act act = QAction('Remove Cycle Marker', self) act.triggered.connect(self.remove_cycle_mrkr) actions['remove_cyc'] = act act = QAction('Clear Cycle Markers', self) act.triggered.connect(self.clear_cycle_mrkrs) actions['clear_cyc'] = act act = QAction('Domino', self) act.triggered.connect(partial(self.import_staging, 'domino')) actions['import_domino'] = act act = QAction('Alice', self) act.triggered.connect(partial(self.import_staging, 'alice')) actions['import_alice'] = act act = QAction('Sandman', self) act.triggered.connect(partial(self.import_staging, 'sandman')) actions['import_sandman'] = act act = QAction('RemLogic', self) act.triggered.connect(partial(self.import_staging, 'remlogic')) actions['import_remlogic'] = act act = QAction('Compumedics', self) act.triggered.connect(partial(self.import_staging, 'compumedics')) actions['import_compumedics'] = act act = QAction('PRANA', self) act.triggered.connect(partial(self.import_staging, 'prana')) actions['import_prana'] = act act = QAction('DeltaMed', self) act.triggered.connect(partial(self.import_staging, 'deltamed')) actions['import_deltamed'] = act act = QAction('Grael', self) act.triggered.connect(partial(self.import_staging, 'grael')) actions['import_grael'] = act act = QAction('Phillips', self) act.triggered.connect(partial(self.import_staging, 'phillips')) actions['import_phillips'] = act act = QAction('FASST', self) act.triggered.connect(self.import_fasst) actions['import_fasst'] = act act = QAction('Domino', self) act.triggered.connect(partial(self.import_staging, 'domino', as_qual=True)) actions['import_domino_qual'] = act act = QAction('Alice', self) act.triggered.connect(partial(self.import_staging, 'alice', as_qual=True)) actions['import_alice_qual'] = act act = QAction('Sandman', self) act.triggered.connect(partial(self.import_staging, 'sandman', as_qual=True)) actions['import_sandman_qual'] = act act = QAction('RemLogic', self) act.triggered.connect(partial(self.import_staging, 'remlogic', as_qual=True)) actions['import_remlogic_qual'] = act act = QAction('Compumedics', self) act.triggered.connect(partial(self.import_staging, 'compumedics', as_qual=True)) actions['import_compumedics_qual'] = act act = QAction('PRANA', self) act.triggered.connect(partial(self.import_staging, 'prana', as_qual=True)) actions['import_prana_qual'] = act act = QAction('DeltaMed', self) act.triggered.connect(partial(self.import_staging, 'deltamed', as_qual=True)) actions['import_deltamed_qual'] = act act = QAction('Phillips', self) act.triggered.connect(partial(self.import_staging, 'phillips', as_qual=True)) actions['import_phillips_qual'] = act act = QAction('Wonambi', self) act.triggered.connect(partial(self.import_events, 'wonambi')) actions['import_events_wonambi'] = act act = QAction('RemLogic', self) act.triggered.connect(partial(self.import_events, 'remlogic')) actions['import_events_remlogic'] = act act = QAction('CSV', self) act.triggered.connect(partial(self.export, xformat='csv')) actions['export_to_csv'] = act act = QAction('RemLogic', self) act.triggered.connect(partial(self.export, xformat='remlogic')) actions['export_to_remlogic'] = act act = QAction('RemLogic FR', self) act.triggered.connect(partial(self.export, xformat='remlogic_fr')) actions['export_to_remlogic_fr'] = act act = QAction('Export Events...', self) act.triggered.connect(self.parent.show_export_events_dialog) actions['export_events'] = act act = QAction('Spindle...', self) act.triggered.connect(self.parent.show_spindle_dialog) act.setShortcut('Ctrl+Shift+s') act.setEnabled(False) actions['spindle'] = act act = QAction('Slow Wave...', self) act.triggered.connect(self.parent.show_slow_wave_dialog) act.setShortcut('Ctrl+Shift+w') act.setEnabled(False) actions['slow_wave'] = act act = QAction('Analysis Console', self) act.triggered.connect(self.parent.show_analysis_dialog) act.setShortcut('Ctrl+Shift+a') act.setEnabled(False) actions['analyze'] = act act = QAction('Sleep Statistics', self) act.triggered.connect(self.export_sleeps_stats) actions['export_sleepstats'] = act self.action = actions
[docs] def update_settings(self): """Once Settings are applied, update the notes.""" self.update_dataset_marker() self.update_annotations()
[docs] def update_notes(self, xml_file, new=False): """Update information about the sleep scoring. Parameters ---------- xml_file : str file of the new or existing .xml file new : bool if the xml_file should be a new file or an existing one """ if new: create_empty_annotations(xml_file, self.parent.info.dataset) self.annot = Annotations(xml_file) else: self.annot = Annotations(xml_file) self.enable_events() self.parent.create_menubar() self.idx_stage.clear() for one_stage in STAGE_NAME: self.idx_stage.addItem(one_stage) self.idx_stage.setCurrentIndex(-1) self.idx_quality.clear() for one_qual in QUALIFIERS: self.idx_quality.addItem(one_qual) self.idx_quality.setCurrentIndex(-1) w1 = self.idx_summary.takeAt(1).widget() w2 = self.idx_summary.takeAt(1).widget() self.idx_summary.removeWidget(w1) self.idx_summary.removeWidget(w2) w1.deleteLater() w2.deleteLater() b1 = QGroupBox('Staging') layout = QFormLayout() for one_stage in STAGE_NAME: layout.addRow(one_stage, QLabel('')) b1.setLayout(layout) self.idx_summary.addWidget(b1) self.idx_stage_stats = layout b2 = QGroupBox('Signal quality') layout = QFormLayout() for one_qual in QUALIFIERS: layout.addRow(one_qual, QLabel('')) b2.setLayout(layout) self.idx_summary.addWidget(b2) self.idx_qual_stats = layout self.display_notes()
[docs] def enable_events(self): """enable slow wave and spindle detection if both annotations and channels are active. """ if self.annot is not None and self.parent.channels.groups: self.action['spindle'].setEnabled(True) self.action['slow_wave'].setEnabled(True) self.action['analyze'].setEnabled(True) else: self.action['spindle'].setEnabled(False) self.action['slow_wave'].setEnabled(False) self.action['analyze'].setEnabled(False)
[docs] def display_notes(self): """Display information about scores and raters. """ if self.annot is not None: short_xml_file = short_strings(basename(self.annot.xml_file)) self.idx_annotations.setText(short_xml_file) # if annotations were loaded without dataset if self.parent.overview.scene is None: self.parent.overview.update() if not self.annot.raters: self.new_rater() self.idx_rater.setText(self.annot.current_rater) self.display_eventtype() self.update_annotations() self.display_stats() self.epoch_length = self.annot.epoch_length
[docs] def display_stats(self): """Display summary statistics about duration in each stage.""" for i, one_stage in enumerate(STAGE_NAME): second_in_stage = self.annot.time_in_stage(one_stage) time_in_stage = str(timedelta(seconds=second_in_stage)) label = self.idx_stage_stats.itemAt(i, QFormLayout.FieldRole).widget() label.setText(time_in_stage) for i, one_qual in enumerate(QUALIFIERS): second_in_qual = self.annot.time_in_stage(one_qual, attr='quality') time_in_qual = str(timedelta(seconds=second_in_qual)) label = self.idx_qual_stats.itemAt(i, QFormLayout.FieldRole).widget() label.setText(time_in_qual)
[docs] def add_bookmark(self, time): """Run this function when user adds a new bookmark. Parameters ---------- time : tuple of float start and end of the new bookmark, in s """ if self.annot is None: # remove if buttons are disabled msg = 'No score file loaded' lg.debug(msg) error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error adding bookmark') error_dialog.showMessage(msg) error_dialog.exec() return answer = QInputDialog.getText(self, 'New Bookmark', 'Enter bookmark\'s name') if answer[1]: name = answer[0] self.annot.add_bookmark(name, time) lg.info('Added Bookmark ' + name + 'at ' + str(time)) self.update_annotations()
[docs] def remove_bookmark(self, time): """User removes bookmark. Parameters ---------- time : tuple of float start and end of the new bookmark, in s """ self.annot.remove_bookmark(time=time) self.update_annotations()
[docs] def update_dataset_marker(self): """Update markers which are in the dataset. It always updates the list of events. Depending on the settings, it might add the markers to overview and traces. """ start_time = self.parent.overview.start_time markers = [] if self.parent.info.markers is not None: markers = self.parent.info.markers self.idx_marker.clearContents() self.idx_marker.setRowCount(len(markers)) for i, mrk in enumerate(markers): abs_time = (start_time + timedelta(seconds=mrk['start'])).strftime('%H:%M:%S') dur = timedelta(seconds=mrk['end'] - mrk['start']) duration = '{0:02d}.{1:03d}'.format(dur.seconds, round(dur.microseconds / 1000)) item_time = QTableWidgetItem(abs_time) item_duration = QTableWidgetItem(duration) item_name = QTableWidgetItem(str(mrk['name'])) color = self.parent.value('marker_color') item_time.setForeground(QColor(color)) item_duration.setForeground(QColor(color)) item_name.setForeground(QColor(color)) self.idx_marker.setItem(i, 0, item_time) self.idx_marker.setItem(i, 1, item_duration) self.idx_marker.setItem(i, 2, item_name) # store information about the time as list (easy to access) marker_start = [mrk['start'] for mrk in markers] marker_end = [mrk['end'] for mrk in markers] self.idx_marker.setProperty('start', marker_start) self.idx_marker.setProperty('end', marker_end) if self.parent.traces.data is not None: self.parent.traces.display() self.parent.overview.display_markers()
[docs] def display_eventtype(self): """Read the list of event types in the annotations and update widgets. """ if self.annot is not None: event_types = sorted(self.annot.event_types, key=str.lower) else: event_types = [] self.idx_eventtype.clear() evttype_group = QGroupBox('Event Types') layout = QVBoxLayout() evttype_group.setLayout(layout) self.check_all_eventtype = check_all = QCheckBox('All event types') check_all.setCheckState(Qt.Checked) check_all.clicked.connect(self.toggle_eventtype) layout.addWidget(check_all) self.idx_eventtype_list = [] for one_eventtype in event_types: self.idx_eventtype.addItem(one_eventtype) item = QCheckBox(one_eventtype) layout.addWidget(item) item.setCheckState(Qt.Checked) item.stateChanged.connect(self.update_annotations) item.stateChanged.connect(self.toggle_check_all_eventtype) self.idx_eventtype_list.append(item) self.idx_eventtype_scroll.setWidget(evttype_group)
[docs] def toggle_eventtype(self): """Check or uncheck all event types in event type scroll.""" check = self.check_all_eventtype.isChecked() for btn in self.idx_eventtype_list: btn.setChecked(check)
[docs] def toggle_check_all_eventtype(self): """Check 'All' if all event types are checked in event type scroll.""" checklist = asarray([btn.isChecked for btn in self.idx_eventtype_list]) if not checklist.all(): self.check_all_eventtype.setChecked(False)
[docs] def get_selected_events(self, time_selection=None): """Returns which events are present in one time window. Parameters ---------- time_selection : tuple of float start and end of the window of interest Returns ------- list of dict list of events in the window of interest """ events = [] for checkbox in self.idx_eventtype_list: if checkbox.checkState() == Qt.Checked: events.extend(self.annot.get_events(name=checkbox.text(), time=time_selection)) return events
[docs] def update_annotations(self): """Update annotations made by the user, including bookmarks and events. Depending on the settings, it might add the bookmarks to overview and traces. """ start_time = self.parent.overview.start_time if self.parent.notes.annot is None: all_annot = [] else: bookmarks = self.parent.notes.annot.get_bookmarks() events = self.get_selected_events() all_annot = bookmarks + events all_annot = sorted(all_annot, key=lambda x: x['start']) self.idx_annot_list.clearContents() self.idx_annot_list.setRowCount(len(all_annot)) for i, mrk in enumerate(all_annot): abs_time = (start_time + timedelta(seconds=mrk['start'])).strftime('%H:%M:%S') dur = timedelta(seconds=mrk['end'] - mrk['start']) duration = '{0:02d}.{1:03d}'.format(dur.seconds, round(dur.microseconds / 1000)) item_time = QTableWidgetItem(abs_time) item_duration = QTableWidgetItem(duration) item_name = QTableWidgetItem(str(mrk['name'])) if mrk in bookmarks: item_type = QTableWidgetItem('bookmark') color = self.parent.value('annot_bookmark_color') else: item_type = QTableWidgetItem('event') color = convert_name_to_color(str(mrk['name'])) chan = mrk['chan'] if isinstance(chan, (tuple, list)): chan = ', '.join(chan) item_chan = QTableWidgetItem(chan) item_time.setForeground(QColor(color)) item_duration.setForeground(QColor(color)) item_name.setForeground(QColor(color)) item_type.setForeground(QColor(color)) item_chan.setForeground(QColor(color)) self.idx_annot_list.setItem(i, 0, item_time) self.idx_annot_list.setItem(i, 1, item_duration) self.idx_annot_list.setItem(i, 2, item_name) self.idx_annot_list.setItem(i, 3, item_type) self.idx_annot_list.setItem(i, 4, item_chan) # store information about the time as list (easy to access) annot_start = [ann['start'] for ann in all_annot] annot_end = [ann['end'] for ann in all_annot] annot_name = [ann['name'] for ann in all_annot] self.idx_annot_list.setProperty('start', annot_start) self.idx_annot_list.setProperty('end', annot_end) self.idx_annot_list.setProperty('name', annot_name) if self.parent.traces.data is not None: self.parent.traces.display_annotations() self.parent.overview.display_annotations()
[docs] def delete_row(self): """Delete bookmarks or event from annotations, based on row.""" sel_model = self.idx_annot_list.selectionModel() for row in sel_model.selectedRows(): i = row.row() start = self.idx_annot_list.property('start')[i] end = self.idx_annot_list.property('end')[i] name = self.idx_annot_list.item(i, 2).text() marker_event = self.idx_annot_list.item(i, 3).text() if marker_event == 'bookmark': self.annot.remove_bookmark(name=name, time=(start, end)) else: self.annot.remove_event(name=name, time=(start, end)) highlight = self.parent.traces.highlight if highlight: self.parent.traces.scene.removeItem(highlight) highlight = None self.parent.traces.event_sel = None self.update_annotations()
[docs] def go_to_marker(self, row, col, table_type): """Move to point in time marked by the marker. Parameters ---------- row : QtCore.int column : QtCore.int table_type : str 'dataset' table or 'annot' table, it works on either """ if table_type == 'dataset': marker_time = self.idx_marker.property('start')[row] marker_end_time = self.idx_marker.property('end')[row] else: marker_time = self.idx_annot_list.property('start')[row] marker_end_time = self.idx_annot_list.property('end')[row] window_length = self.parent.value('window_length') if self.parent.traces.action['centre_event'].isChecked(): window_start = (marker_time + marker_end_time - window_length) / 2 else: window_start = floor(marker_time / window_length) * window_length self.parent.overview.update_position(window_start) if table_type == 'annot': for annot in self.parent.traces.idx_annot: if annot.marker.x() == marker_time: self.parent.traces.highlight_event(annot) break
[docs] def find_row(self, ev_start, ev_end): """Highlight event row in table from start and end time. Parameters ---------- ev_start : float start time, in seconds from record start ev_end : float end time, in seconds from record start Returns ------- int index of event row in idx_annot_list QTableWidget """ all_starts = self.idx_annot_list.property('start') all_ends = self.idx_annot_list.property('end') for i, (start, end) in enumerate(zip(all_starts, all_ends)): if start == ev_start and end == ev_end: return i for i, start in enumerate(all_starts): if start == ev_start: return i for i, end in enumerate(all_ends): if end == ev_end: return i raise ValueError
[docs] def get_sleepstage(self, stage_idx=None): """Score the sleep stage, using shortcuts or combobox.""" if self.annot is None: # remove if buttons are disabled error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error getting sleep stage') error_dialog.showMessage('No score file loaded') error_dialog.exec() return window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') if window_length != self.epoch_length: msg = ('Zoom to ' + str(self.epoch_length) + ' (epoch length) ' + 'for sleep scoring.') error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error getting sleep stage') error_dialog.showMessage(msg) error_dialog.exec() lg.debug(msg) return try: self.annot.set_stage_for_epoch(window_start, STAGE_NAME[stage_idx]) except KeyError: msg = ('The start of the window does not correspond to any epoch ' + 'in sleep scoring file.\n\n' 'Switch to the appropriate window length in View, then use ' 'Navigation --> Line Up with Epoch to line up the window.') error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error getting sleep stage') error_dialog.showMessage(msg) error_dialog.exec() lg.debug(msg) else: lg.debug('User staged ' + str(window_start) + ' as ' + STAGE_NAME[stage_idx]) self.set_stage_index() self.parent.overview.mark_stages(window_start, window_length, STAGE_NAME[stage_idx]) self.display_stats() self.parent.traces.page_next()
[docs] def get_quality(self, qual_idx=None): """Get the signal qualifier, using shortcuts or combobox.""" if self.annot is None: # remove if buttons are disabled msg = 'No score file loaded' error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error getting quality') error_dialog.showMessage(msg) error_dialog.exec() lg.debug(msg) return window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') try: self.annot.set_stage_for_epoch(window_start, QUALIFIERS[qual_idx], attr='quality') except KeyError: msg = ('The start of the window does not correspond to any epoch ' + 'in sleep scoring file') error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error getting quality') error_dialog.showMessage(msg) error_dialog.exec() lg.debug(msg) else: lg.debug('User staged ' + str(window_start) + ' as ' + QUALIFIERS[qual_idx]) self.set_quality_index() self.parent.overview.mark_quality(window_start, window_length, QUALIFIERS[qual_idx]) self.display_stats() self.parent.traces.page_next()
[docs] def get_cycle_mrkr(self, end=False): """Mark cycle start or end. Parameters ---------- end : bool If True, marks a cycle end; otherwise, it's a cycle start """ if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') try: self.annot.set_cycle_mrkr(window_start, end=end) except KeyError: msg = ('The start of the window does not correspond to any epoch ' 'in sleep scoring file') self.parent.statusBar().showMessage(msg) lg.debug(msg) else: bound = 'start' if end: bound = 'end' lg.info('User marked ' + str(window_start) + ' as cycle ' + bound) self.parent.overview.mark_cycles(window_start, window_length, end=end)
[docs] def remove_cycle_mrkr(self): """Remove cycle marker.""" window_start = self.parent.value('window_start') try: self.annot.remove_cycle_mrkr(window_start) except KeyError: msg = ('The start of the window does not correspond to any cycle ' 'marker in sleep scoring file') self.parent.statusBar().showMessage(msg) lg.debug(msg) else: lg.debug('User removed cycle marker at' + str(window_start)) #self.trace self.parent.overview.update(reset=False) self.parent.overview.display_annotations()
[docs] def clear_cycle_mrkrs(self, test=False): """Remove all cycle markers.""" if not test: msgBox = QMessageBox(QMessageBox.Question, 'Clear Cycle Markers', 'Are you sure you want to remove all cycle ' 'markers for this rater?') msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.Yes) response = msgBox.exec_() if response == QMessageBox.No: return self.annot.clear_cycles() self.parent.overview.display() self.parent.overview.display_annotations()
[docs] def set_stage_index(self): """Set the current stage in combobox.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') stage = self.annot.get_stage_for_epoch(window_start, window_length) #lg.info('winstart: ' + str(window_start) + ', stage: ' + str(stage)) if stage is None: self.idx_stage.setCurrentIndex(-1) else: self.idx_stage.setCurrentIndex(STAGE_NAME.index(stage))
[docs] def set_quality_index(self): """Set the current signal quality in combobox.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') qual = self.annot.get_stage_for_epoch(window_start, window_length, attr='quality') #lg.info('winstart: ' + str(window_start) + ', quality: ' + str(qual)) if qual is None: self.idx_quality.setCurrentIndex(-1) else: self.idx_quality.setCurrentIndex(QUALIFIERS.index(qual))
[docs] def new_annot(self): """Action: create a new file for annotations.""" if self.parent.info.filename is None: msg = 'No dataset loaded' self.parent.statusBar().showMessage(msg) lg.debug(msg) return filename = splitext(self.parent.info.filename)[0] + '_scores.xml' filename, _ = QFileDialog.getSaveFileName(self, 'Create annotation file', filename, 'Annotation File (*.xml)') if filename == '': return self.update_notes(filename, True)
[docs] def load_annot(self): """Action: load a file for annotations.""" if self.parent.info.filename is not None: filename = splitext(self.parent.info.filename)[0] + '_scores.xml' else: filename = None filename, _ = QFileDialog.getOpenFileName(self, 'Load annotation file', filename, 'Annotation File (*.xml)') if filename == '': return try: self.update_notes(filename, False) except FileNotFoundError: msg = 'Annotation file not found' self.parent.statusBar().showMessage(msg) lg.info(msg)
[docs] def clear_annot(self): """Action: clear all the annotations (ask for confirmation first).""" msgBox = QMessageBox(QMessageBox.Question, 'Clear Annotations', 'Do you want to remove all the annotations?') msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.Yes) response = msgBox.exec_() if response == QMessageBox.No: return self.reset()
[docs] def import_fasst(self, checked=False, test_fasst=None, test_annot=None): """Action: import from FASST .mat file""" if self.parent.info.filename is not None: fasst_file = splitext(self.parent.info.filename)[0] + '.mat' annot_file = splitext(self.parent.info.filename)[0] + '_scores.xml' else: fasst_file = annot_file = '' if test_fasst is None: fasst_file, _ = QFileDialog.getOpenFileName(self, 'Load FASST score file', fasst_file, 'FASST score file (*.mat)') else: fasst_file = test_fasst if fasst_file == '': return if test_annot is None: annot_file, _ = QFileDialog.getSaveFileName(self, 'Create annotation file', annot_file, 'Annotation File (*.xml)') else: annot_file = test_annot if annot_file == '': return try: create_annotation(annot_file, from_fasst=fasst_file) except BaseException as err: self.parent.statusBar().showMessage(str(err)) lg.info(str(err)) return try: self.update_notes(annot_file, False) except FileNotFoundError: msg = 'Annotation file not found' self.parent.statusBar().showMessage(msg) lg.info(msg)
[docs] def import_staging(self, source, staging_start=None, as_qual=False, test_filename=None, test_rater=None): """Action: import an external sleep staging file. Parameters ---------- source : str Name of program where staging was exported. One of 'alice', 'compumedics', 'domino', 'remlogic', 'sandman'. staging_start : datetime, optional Absolute time when staging begins. as_qual : bool if True, scores will be imported as quality """ if self.annot is None: # remove if buttons are disabled msg = 'No score file loaded' self.parent.statusBar().showMessage(msg) lg.info(msg) return if self.parent.info.dataset is None: msg = 'No dataset loaded' self.parent.statusBar().showMessage(msg) lg.info(msg) return record_start = self.parent.info.dataset.header['start_time'] file_filter = 'Text File (*.txt)' if source == 'phillips': file_filter = 'CSV File (*.csv)' if test_filename is None: filename, _ = QFileDialog.getOpenFileName(self, 'Load staging file', None, file_filter) else: filename = test_filename if filename == '': return if test_rater is None: rater, ok = QInputDialog.getText(self, 'Import staging', 'Enter rater name') if not ok: return if rater in self.annot.raters and not as_qual: msgBox = QMessageBox(QMessageBox.Question, 'Overwrite staging', 'Rater %s already exists. \n \n' 'Overwrite %s\'s sleep staging ' 'with imported staging? Events ' 'and bookmarks will be preserved.' % (rater, rater)) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.No) response = msgBox.exec_() if response == QMessageBox.No: return else: rater = test_rater if source in ['deltamed', 'compumedics', 'grael']: time_str, ok = QInputDialog.getText(self, 'Staging start time', 'Enter date and time when ' 'staging \nbegins, using ' '24-hour clock. \n\nFormat: ' 'YYYY,MM,DD HH:mm:SS') if not ok: return try: staging_start = datetime.strptime(time_str, '%Y,%m,%d %H:%M:%S') except (ValueError, TypeError) as e: msg = 'Incorrect formatting for date and time.' self.parent.statusBar().showMessage(msg) lg.info(msg) return poor = ['Artefact', 'Artifact'] if as_qual: query = 'Which epoch label should be read as Poor quality signal?' poor, ok = QInputDialog.getText(self, 'Import quality', query) poor = [poor] if not ok: return try: unaligned = self.annot.import_staging(filename, source, rater, record_start, staging_start=staging_start, poor=poor, as_qual=as_qual) except FileNotFoundError: msg = 'File not found' self.parent.statusBar().showMessage(msg) lg.info(msg) if unaligned: msg = 'Imported scores are not aligned with existing scores.' self.parent.statusBar().showMessage(msg) lg.info(msg) self.display_notes() self.parent.create_menubar() # refresh list ot raters
[docs] def new_rater(self): """Action: add a new rater. """ if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return newuser = NewUserDialog(self.parent.value('scoring_window')) answer = newuser.exec_() if answer == QDialog.Rejected: return rater_name = newuser.rater_name.text() if rater_name != '': self.annot.add_rater(rater_name, newuser.epoch_length.value()) self.display_notes() self.parent.create_menubar() # refresh list ot raters
[docs] def select_rater(self, rater=False): """Action: select one rater. Parameters ---------- rater : str name of the rater """ self.annot.get_rater(rater) self.display_notes() self.set_stage_index() self.set_quality_index()
[docs] def delete_rater(self): """Action: create dialog to delete rater.""" answer = QInputDialog.getText(self, 'Delete Rater', 'Enter rater\'s name') if answer[1]: self.annot.remove_rater(answer[0]) self.display_notes() self.parent.create_menubar() # refresh list ot raters
[docs] def rename_rater(self, test_name=None, test_new_name=None): """action: create dialog to rename event type.""" if test_name and test_new_name: name = test_name, True new_name = test_new_name, True else: name = QInputDialog.getText(self, 'Rename Rater', 'Enter name of rater to rename.') if name[1]: new_name = QInputDialog.getText(self, 'Rename Rater', 'Enter new name for rater.') if new_name[1]: self.annot.rename_rater(name[0], new_name[0]) self.display_notes() self.parent.create_menubar() # refresh list ot raters
[docs] def new_eventtype(self, test_type_str=None): """Action: create dialog to add new event type.""" if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return if test_type_str: answer = test_type_str, True else: answer = QInputDialog.getText(self, 'New Event Type', 'Enter new event\'s name') if answer[1]: self.annot.add_event_type(answer[0]) self.display_eventtype() n_eventtype = self.idx_eventtype.count() self.idx_eventtype.setCurrentIndex(n_eventtype - 1) return answer
[docs] def delete_eventtype(self, test_type_str=None): """Action: create dialog to delete event type.""" if test_type_str: answer = test_type_str, True else: answer = QInputDialog.getText(self, 'Delete Event Type', 'Enter event\'s name to delete') if answer[1]: self.annot.remove_event_type(answer[0]) self.display_eventtype() self.update_annotations()
[docs] def rename_eventtype(self, test_name=None, test_new_name=None): """action: create dialog to rename event type.""" if test_name and test_new_name: name = test_name, True new_name = test_new_name, True else: name = QInputDialog.getText(self, 'Rename Event Type', 'Enter name of event type to rename.') if name[1]: new_name = QInputDialog.getText(self, 'Rename Event Type', 'Enter new name for event type.') if new_name[1]: self.annot.rename_event_type(name[0], new_name[0]) self.display_eventtype() self.update_annotations()
[docs] def add_event(self, name, time, chan): """Action: add a single event.""" self.annot.add_event(name, time, chan=chan) self.update_annotations()
[docs] def remove_event(self, name=None, time=None, chan=None): """Action: remove single event.""" self.annot.remove_event(name=name, time=time, chan=chan) self.update_annotations()
[docs] def change_event_type(self, new_name=None, name=None, time=None, chan=None): """Action: change an event's type.""" if new_name is None: event_types = self.annot.event_types if name is None: event = self.annot.get_events(time=time, chan=chan)[0] name = event['name'] idx_name = event_types.index(name) if idx_name == len(event_types) - 1: new_name = event_types[0] else: new_name = event_types[idx_name + 1] self.annot.add_event(new_name, time, chan=chan) self.annot.remove_event(name=name, time=time, chan=chan) self.update_annotations() return new_name
[docs] def markers_to_events(self, keep_name=False): """Copy all markers in dataset to event type. """ markers = self.parent.info.markers if markers is None: self.parent.statusBar.showMessage('No markers in dataset.') return if not keep_name: name, ok = self.new_eventtype() if not ok: return else: name = None self.annot.add_events(markers, name=name, chan='') if keep_name: self.display_eventtype() n_eventtype = self.idx_eventtype.count() self.idx_eventtype.setCurrentIndex(n_eventtype - 1) self.update_annotations()
[docs] def detect_events(self, data, method, params, label): """Detect events and display on signal. Parameters ---------- data : instance of ChanTime one segment with all channels of interest method : str Method used for detection. params : dict Parameters used for detection. label : str Name of event type, on event labels """ if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return lg.info('Adding event type ' + label) self.annot.add_event_type(label) self.display_eventtype() n_eventtype = self.idx_eventtype.count() self.idx_eventtype.setCurrentIndex(n_eventtype - 1) if params['max_dur'] in [0, 'None']: params['max_dur'] = None freq = (float(params['f1']), float(params['f2'])) duration = (params['min_dur'], params['max_dur']) if method in SPINDLE_METHODS: detector = DetectSpindle(method=method, frequency=freq, duration=duration, merge=params['merge']) detector.rolloff = params['rolloff'] detector.min_interval = params['interval'] if 'Ferrarelli2007' == method: detector.det_thresh = params['0'] detector.sel_thresh = params['1'] if 'Nir2011' == method: detector.smooth['dur'] = params['0'] detector.det_thresh = params['1'] detector.sel_thresh = params['2'] if 'Moelle2011' == method: detector.moving_rms['dur'] = params['0'] detector.smooth['dur'] = params['1'] detector.det_thresh = params['2'] if 'Wamsley2012' == method: detector.det_wavelet['dur'] = params['0'] detector.det_wavelet['sd'] = params['1'] detector.smooth['dur'] = params['2'] detector.det_thresh = params['3'] if 'Martin2013' == method: detector.moving_rms['dur'] = params['0'] detector.moving_rms['step'] = params['1'] detector.det_thresh = params['2'] if 'Ray2015' == method: detector.smooth['dur'] = params['0'] detector.zscore['step'] = params['1'] detector.det_thresh = params['2'] detector.sel_thresh = params['3'] if 'Lacourse2018' == method: detector.windowing['dur'] = params['0'] detector.windowing['step'] = params['1'] detector.abs_pow_thresh = params['2'] detector.rel_pow_thresh = params['3'] detector.covar_thresh = params['4'] detector.corr_thresh = params['5'] if 'FASST' == method: detector.det_thresh = params['0'] detector.smooth['dur'] = params['1'] if 'FASST2' == method: detector.det_thresh = params['0'] detector.moving_rms['dur'] = params['1'] detector.smooth['dur'] = params['2'] if 'UCSD' == method: detector.det_wavelet['dur'] = params['0'] detector.det_wavelet['width'] = params['1'] detector.det_wavelet['win'] = params['2'] detector.det_thresh = params['3'] detector.sel_thresh = params['4'] if 'Concordia' == method: detector.moving_rms['dur'] = params['0'] detector.smooth['dur'] = params['1'] detector.det_thresh = params['2'] detector.det_thresh_hi = params['3'] detector.tolerance = params['4'] detector.sel_thresh = params['5'] elif method in SLOW_WAVE_METHODS: detector = DetectSlowWave(method=method, duration=duration) detector.det_filt['freq'] = freq detector.trough_duration = (params['min_trough_dur'], params['max_trough_dur']) detector.max_trough_amp = params['max_trough_amp'] detector.min_ptp = params['min_ptp'] detector.invert = params['invert'] else: lg.info('Method not recognized: ' + method) return events = detector(data, parent=self) if events: self.annot.add_events(events, name=label) self.update_annotations()
[docs] def export(self, xformat='csv'): """action: export annotations to CSV.""" if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return if xformat == 'csv': filename = splitext(self.annot.xml_file)[0] + '.csv' filename, _ = QFileDialog.getSaveFileName(self, 'Export stages', filename, 'Sleep stages (*.csv)') if 'remlogic' in xformat: filename = splitext(self.annot.xml_file)[0] + '.txt' filename, _ = QFileDialog.getSaveFileName(self, 'Export stages', filename, 'Sleep stages (*.txt)') if filename == '': return self.annot.export(filename, xformat=xformat)
[docs] def import_events(self, source='wonambi'): """action: import events from text file (Wonambi or RemLogic).""" if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return if 'wonambi' == source: format_str = 'CSV File (*.csv)' rec_start = None elif 'remlogic' == source: format_str = 'Text file (*.txt)' rec_start = self.parent.info.dataset.header['start_time'] fn, _ = QFileDialog.getOpenFileName(self, 'Import events', None, format_str) if fn == '': return fn = Path(fn).resolve() self.annot.import_events(fn, source=source, rec_start=rec_start, parent=self.parent) self.display_notes()
[docs] def export_sleeps_stats(self): """action: export sleep statistics CSV.""" if self.annot is None: # remove if buttons are disabled self.parent.statusBar().showMessage('No score file loaded') return fn = splitext(self.annot.xml_file)[0] + '_sleepstats.csv' fn, _ = QFileDialog.getSaveFileName(self, 'Export sleep statistics', fn, 'Sleep stats (*.csv)') if fn == '': return dt_dialog = DateTimeDialog('Lights OFF', self.annot.start_time, self.annot.last_second) if not dt_dialog.exec(): return lights_off = float(dt_dialog.idx_seconds.value()) dt_dialog = DateTimeDialog('Lights ON', self.annot.start_time, self.annot.last_second) if not dt_dialog.exec(): return lights_on = float(dt_dialog.idx_seconds.value()) if self.annot.export_sleep_stats(fn, lights_off, lights_on) is None: self.parent.statusBar().showMessage('No epochs scored as sleep.')
[docs] def reset(self): """Remove all annotations from window.""" self.idx_annotations.setText('Load Annotation File...') self.idx_rater.setText('') self.annot = None self.dataset_markers = None # remove dataset marker self.idx_marker.clearContents() self.idx_marker.setRowCount(0) # remove summary statistics w1 = self.idx_summary.takeAt(1).widget() w2 = self.idx_summary.takeAt(1).widget() self.idx_summary.removeWidget(w1) self.idx_summary.removeWidget(w2) w1.deleteLater() w2.deleteLater() b1 = QGroupBox('Staging') b2 = QGroupBox('Signal quality') self.idx_summary.addWidget(b1) self.idx_summary.addWidget(b2) # remove annotations self.display_eventtype() self.update_annotations() self.parent.create_menubar() # remove all raters
[docs] def reset_current_row(self): """For traces.next_event""" self.parent.traces.current_event_row = None
[docs]class MergeDialog(QDialog): """Dialog for specifying which events to merge. Events are merged when less than a minimum interval separates them. Events can be within-channel only or both within- and across-channel. Merged to channel with longest event. Attributes ---------- parent : instance of QMainWindow the main window idx_evt_type : QListWidget List of event types. idx_merge_to : FormMenu Choice of 'earlier onset event' or 'longer duration event'. merge_to : str Current text in idx_merge_to min_interval : FormFloat Events separated by this value (in seconds) or less are merged. cross_chan: FormBool For cross-channel merging. """ def __init__(self, parent): super().__init__(None, Qt.WindowSystemMenuHint | Qt.WindowTitleHint) self.parent = parent self.setWindowTitle('Merge events') self.setWindowModality(Qt.ApplicationModal) self.create_dialog()
[docs] def create_dialog(self): """ Create the dialog.""" box0 = QGroupBox('Info') event_box = QListWidget() event_box.setSelectionMode(QAbstractItemView.ExtendedSelection) self.idx_evt_type = event_box self.idx_merge_to = FormMenu(['earlier onset event', 'longer duration event']) self.merge_to = self.idx_merge_to.currentText() self.min_interval = FormFloat() self.cross_chan = FormBool('Merge across channels') self.min_interval.set_value(1.0) self.cross_chan.setCheckState(Qt.Checked) flayout = QFormLayout() box0.setLayout(flayout) flayout.addRow('Event type(s)', self.idx_evt_type) flayout.addRow(self.cross_chan) flayout.addRow('Merge to...', self.idx_merge_to) flayout.addRow('Min. interval (sec)', self.min_interval) bbox = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.idx_ok = bbox.button(QDialogButtonBox.Ok) self.idx_cancel = bbox.button(QDialogButtonBox.Cancel) bbox.clicked.connect(self.button_clicked) btnlayout = QHBoxLayout() btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(box0) vlayout.addStretch(1) vlayout.addLayout(btnlayout) self.setLayout(vlayout)
[docs] def button_clicked(self, button): """Action when button was clicked. Parameters ---------- button : instance of QPushButton which button was pressed """ if button is self.idx_ok: evt_types = [x.text() for x in self.idx_evt_type.selectedItems()] self.merge_to = self.idx_merge_to.currentText() min_interval = self.min_interval.get_value() events = [] merge_to_longer = False if not evt_types: QMessageBox.warning(self, 'Missing information', 'Choose at least one event type.') return min_interval = 0 if not min_interval else min_interval if self.merge_to == 'longer duration event': merge_to_longer = True if len(evt_types) > 1: answer = QInputDialog.getText(self, 'New Event Type', 'Enter new event\'s name') if answer[1]: name = answer[0] else: return else: name = evt_types[0] for etype in evt_types: events.extend(self.parent.notes.annot.get_events(name=etype, qual='Good')) if self.cross_chan.get_value(): events = merge_close(events, min_interval, merge_to_longer=merge_to_longer) else: channels = sorted(set([y for x in events for y in x['chan']])) events = [] chan_events = [] for chan in channels: chan_events = [] for etype in evt_types: chan_events.extend(self.parent.notes.annot.get_events( name=etype, chan=chan, qual='Good')) events.extend(merge_close(chan_events, min_interval, merge_to_longer=merge_to_longer)) for etype in evt_types: self.parent.notes.annot.remove_event_type(etype) self.parent.notes.add_events(events, name=name, chan=None) self.parent.notes.display_eventtype() n_eventtype = self.parent.notes.idx_eventtype.count() self.parent.notes.idx_eventtype.setCurrentIndex(n_eventtype - 1) self.accept() if button is self.idx_cancel: self.reject()
[docs] def update_event_types(self): """Update event types in event type box.""" self.idx_evt_type.clear() self.idx_evt_type.setSelectionMode(QAbstractItemView.ExtendedSelection) event_types = sorted(self.parent.notes.annot.event_types, key=str.lower) for ty in event_types: item = QListWidgetItem(ty) self.idx_evt_type.addItem(item)
[docs]class NewUserDialog(QDialog): def __init__(self, default_length): super().__init__() bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.idx_ok = bbox.button(QDialogButtonBox.Ok) self.idx_cancel = bbox.button(QDialogButtonBox.Cancel) bbox.clicked.connect(self.button_clicked) self.rater_name = QLineEdit('') self.epoch_length = QSpinBox() self.epoch_length.setValue(default_length) f = QFormLayout() f.addRow('Rater\'s name', self.rater_name) f.addRow('Epoch Length', self.epoch_length) f.addRow(bbox) self.setLayout(f) self.show()
[docs] def button_clicked(self, button): if button == self.idx_ok: self.accept() elif button == self.idx_cancel: self.reject()
[docs]class ExportEventsDialog(QDialog): """Dialog for choosing export dataset options.""" def __init__(self, parent): super().__init__(None, Qt.WindowSystemMenuHint | Qt.WindowTitleHint) self.parent = parent self.setWindowTitle('Export events') self.setWindowModality(Qt.WindowModal) self.filename = None self.create_dialog()
[docs] def create_dialog(self): """Create the dialog.""" bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.idx_ok = bbox.button(QDialogButtonBox.Ok) self.idx_cancel = bbox.button(QDialogButtonBox.Cancel) filebutton = QPushButton() filebutton.setText('Choose') self.idx_filename = filebutton self.xp_format = FormMenu(['CSV', 'Brain Vision']) self.all_types = FormBool('All event types') self.idx_evt_type = QListWidget() self.idx_evt_type.setSelectionMode(QAbstractItemView.ExtendedSelection) filebutton.clicked.connect(self.save_as) self.all_types.connect(self.toggle_buttons) bbox.clicked.connect(self.button_clicked) form = QFormLayout() form.addRow('Filename', self.idx_filename) form.addRow('Format', self.xp_format) form.addRow(self.all_types) form.addRow('Event type(s)', self.idx_evt_type) btnlayout = QHBoxLayout() btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addLayout(form) vlayout.addStretch(1) vlayout.addLayout(btnlayout) self.setLayout(vlayout)
[docs] def button_clicked(self, button): """Action when button was clicked. Parameters ---------- button : instance of QPushButton which button was pressed """ if button is self.idx_ok: fn = Path(self.filename) xp_format = self.xp_format.get_value() if self.all_types.get_value(): evt_type = self.event_types else: evt_type = [ x.text() for x in self.idx_evt_type.selectedItems()] if 'CSV' == xp_format: self.parent.notes.annot.export_events(fn, evt_type) elif 'Brain Vision' == xp_format: events = [] for et in evt_type: events.extend(self.parent.notes.annot.get_events(name=et)) if not events: self.parent.statusBar.showMessage('No events found.') return events = sorted(events, key=lambda x: x['start']) dataset = self.parent.info.dataset data = ChanTime() data.start_time = dataset.header['start_time'] data.s_freq = int(dataset.header['s_freq']) with fn.with_suffix('.vmrk').open('w') as f: lg.info('Writing to ' + str(fn) + '.vmrk') f.write(_write_vmrk(data, fn, events)) self.accept() if button is self.idx_cancel: self.reject()
[docs] def update(self): """Update the event types list, info, when dialog is opened.""" self.filename = self.parent.notes.annot.xml_file self.event_types = self.parent.notes.annot.event_types self.idx_evt_type.clear() for ev in self.event_types: self.idx_evt_type.addItem(ev)
[docs] def toggle_buttons(self): """Turn buttons on and off.""" all_types = self.all_types.get_value() self.idx_evt_type.setEnabled(not all_types)
[docs] def save_as(self): """Dialog for getting name, location of dataset export.""" filename = splitext(self.filename)[0] filename, _ = QFileDialog.getSaveFileName(self, 'Export events', filename) if filename == '': return self.filename = filename short_filename = short_strings(basename(self.filename)) self.idx_filename.setText(short_filename)