@@ -175,7 +175,7 @@ def __init__(self, bidsfolder: Path, input_bidsmap: BidsMap, template_bidsmap: B
175175 tabwidget .setTabPosition (QtWidgets .QTabWidget .TabPosition .North )
176176 tabwidget .setTabShape (QtWidgets .QTabWidget .TabShape .Rounded )
177177
178- self .subses_table = {}
178+ self .participant_table = {}
179179 self .samples_table = {}
180180 self .options_label = {}
181181 self .options_table = {}
@@ -250,7 +250,7 @@ def samples_menu(self, pos):
250250 """Pops up a context-menu for deleting or editing the right-clicked sample in the samples_table"""
251251
252252 # Get the activated row-data
253- dataformat = self .tabwidget .widget ( self . tabwidget . currentIndex () ).objectName ()
253+ dataformat = self .tabwidget .currentWidget ( ).objectName ()
254254 table = self .samples_table [dataformat ]
255255 colindex = table .currentColumn ()
256256 rowindexes = [index .row () for index in table .selectedIndexes () if index .column () == colindex ]
@@ -303,7 +303,7 @@ def samples_menu(self, pos):
303303 else :
304304 self .ordered_file_index [dataformat ][datasource .path ] = max (self .ordered_file_index [dataformat ][fname ] for fname in self .ordered_file_index [dataformat ]) + 1
305305 if runitem :
306- self .update_subses_samples (dataformat )
306+ self .fill_samples_table (dataformat )
307307
308308 elif action == delete :
309309 deleted = False
@@ -319,7 +319,7 @@ def samples_menu(self, pos):
319319 self .output_bidsmap .delete_run (provenance , datatype , dataformat )
320320 deleted = True
321321 if deleted :
322- self .update_subses_samples (dataformat )
322+ self .fill_samples_table (dataformat )
323323
324324 elif action == compare :
325325 CompareWindow (runitems , subids , sesids )
@@ -353,7 +353,7 @@ def samples_menu(self, pos):
353353 self .output_bidsmap .update (datatype , templaterun )
354354 LOGGER .verbose (f"User sets run-item { datatype } -> { templaterun } " )
355355
356- self .update_subses_samples (dataformat )
356+ self .fill_samples_table (dataformat )
357357
358358 def set_menu_statusbar (self ):
359359 """Set up the menu and statusbar"""
@@ -427,25 +427,25 @@ def set_tab_bidsmap(self, dataformat: str):
427427 """Set the SOURCE file sample listing tab"""
428428
429429 # Set the Participant table
430- subses_label = QLabel ('Participant data' )
431- subses_label .setToolTip ('Subject /session mappings ' )
432-
433- subses_table = MyQTable (ncols = 2 , nrows = 2 )
434- subses_table .setToolTip (f"Use e.g. '<<filepath:/sub-(.*?)/>>' to parse the subject and (optional) session label from the pathname. NB: the () parentheses indicate the part that is extracted as the subject/session label\n "
435- f"Use a dynamic { dataformat } attribute (e.g. '<<PatientName>>') to extract the subject and (optional) session label from the { dataformat } header" )
436- subses_table . setMouseTracking ( True )
437- header = subses_table .horizontalHeader ()
430+ participant_label = QLabel ('Participant data' )
431+ participant_label .setToolTip ('Data to parse the subject /session labels, and to populate the participants tsv- and json-files ' )
432+
433+ self . participant_table [ dataformat ] = participant_table = MyQTable (ncols = 3 )
434+ participant_table .setToolTip (f"Use e.g. '<<filepath:/sub-(.*?)/>>' to parse the subject and (optional) session label from the pathname. NB: the () parentheses indicate the part that is extracted as the subject/session label\n "
435+ f"Use a dynamic { dataformat } attribute (e.g. '<<PatientName>>') to extract the subject and (optional) session label from the { dataformat } header" )
436+ participant_table . cellChanged . connect ( self . participant_table2bidsmap )
437+ header = participant_table .horizontalHeader ()
438438 header .setSectionResizeMode (0 , QHeaderView .ResizeMode .ResizeToContents )
439439 header .setSectionResizeMode (1 , QHeaderView .ResizeMode .Stretch )
440- subses_table . cellChanged . connect ( self . subsescell2bidsmap )
441- self .subses_table [ dataformat ] = subses_table
440+
441+ self .fill_participant_table ( dataformat )
442442
443443 # Set the bidsmap table
444- label = QLabel ('Data samples' )
445- label .setToolTip ('List of unique source-data samples' )
444+ label = QLabel ('Representative samples' )
445+ label .setToolTip ('List of unique source-data samples (datatypes) ' )
446446
447447 self .samples_table [dataformat ] = samples_table = MyQTable (minsize = False , ncols = 6 )
448- samples_table .setMouseTracking (True )
448+ samples_table .setMouseTracking (True ) # Needed for showing filepath in the statusbar
449449 samples_table .setShowGrid (True )
450450 samples_table .setHorizontalHeaderLabels (['' , f'{ dataformat } input' , 'BIDS data type' , 'BIDS output' , 'Action' , 'Provenance' ])
451451 samples_table .setSortingEnabled (True )
@@ -462,8 +462,8 @@ def set_tab_bidsmap(self, dataformat: str):
462462 header .setSectionResizeMode (3 , QHeaderView .ResizeMode .Stretch )
463463
464464 layout = QVBoxLayout ()
465- layout .addWidget (subses_label )
466- layout .addWidget (subses_table )
465+ layout .addWidget (participant_label )
466+ layout .addWidget (participant_table )
467467 layout .addWidget (label )
468468 layout .addWidget (samples_table )
469469 tab = QtWidgets .QWidget ()
@@ -472,7 +472,7 @@ def set_tab_bidsmap(self, dataformat: str):
472472 self .tabwidget .addTab (tab , f"{ dataformat } mappings" )
473473 self .tabwidget .setCurrentWidget (tab )
474474
475- self .update_subses_samples (dataformat )
475+ self .fill_samples_table (dataformat )
476476
477477 def set_tab_options (self ):
478478 """Set the options tab"""
@@ -556,24 +556,86 @@ def set_tab_filebrowser(self):
556556
557557 self .tabwidget .addTab (tab , 'Data browser' )
558558
559- def update_subses_samples (self , dataformat : str ):
560- """(Re)populates the sample list with bidsnames according to the bidsmap"""
559+ def fill_participant_table (self , dataformat : str ):
560+ """(Re)populate the participant table with the new bidsmap data"""
561+
562+ # Populate the participant table
563+ participant_table = self .participant_table [dataformat ]
564+ participant_table .blockSignals (True )
565+ participant_table .hide ()
566+ participant_table .setRowCount (len (self .output_bidsmap .dataformat (dataformat ).participant ) + 1 )
567+ participant_table .clearContents ()
568+ for n , (key , item ) in enumerate (self .output_bidsmap .dataformat (dataformat ).participant .items ()):
569+ tableitem = MyQTableItem (key , editable = key not in ('participant_id' ,'session_id' ))
570+ tableitem .setToolTip (get_columnhelp (key ))
571+ participant_table = self .participant_table [dataformat ]
572+ participant_table .setItem (n , 0 , tableitem )
573+ participant_table .setItem (n , 1 , MyQTableItem (item ['value' ]))
574+ edit_button = QPushButton ('Metadata' )
575+ edit_button .setToolTip ('Data for participants json sidecar-file' )
576+ edit_button .clicked .connect (self .edit_metadata )
577+ participant_table .setCellWidget (n , 2 , edit_button )
578+
579+ participant_table .show ()
580+ participant_table .blockSignals (False )
581+
582+ def participant_table2bidsmap (self , rowindex : int , colindex : int ):
583+ """
584+ A value has been changed in the participant table. If it is valid, save it to the bidsmap and update the
585+ participant and samples table
586+
587+ :param rowindex:
588+ :param colindex:
589+ :return:
590+ """
591+
592+ # Only if cell was actually clicked, update
593+ if self .tabwidget .currentIndex () < 0 :
594+ return
595+ dataformat = self .tabwidget .currentWidget ().objectName ()
596+ if colindex == 1 and dataformat in self .dataformats :
597+ key = self .participant_table [dataformat ].item (rowindex , 0 ).text ().strip ()
598+ value = self .participant_table [dataformat ].item (rowindex , 1 ).text ().strip ()
599+ oldvalue = self .output_bidsmap .dataformat (dataformat ).participant [key ]['value' ]
600+ if oldvalue is None :
601+ oldvalue = ''
602+
603+ # Only if cell content was changed, update
604+ if key and value != oldvalue :
605+ LOGGER .verbose (f"User sets { dataformat } ['{ key } '] from '{ oldvalue } ' to '{ value } '" )
606+ self .output_bidsmap .dataformat (dataformat ).participant [key ]['value' ] = value
607+ self .fill_participant_table (dataformat )
608+ self .fill_samples_table (dataformat )
609+ self .datachanged = True
610+
611+ def edit_metadata (self ):
612+ """Pop-up a text window to edit the sidecar metadata of participant item"""
613+
614+ dataformat = self .tabwidget .currentWidget ().objectName ()
615+ participant_table = self .participant_table [dataformat ]
616+ clicked = self .focusWidget ()
617+ rowindex = participant_table .indexAt (clicked .pos ()).row ()
618+ key = participant_table .item (rowindex , 0 ).text ().strip ()
619+ meta = self .output_bidsmap .dataformat (dataformat ).participant [key ]['meta' ]
620+
621+ text , ok = QtWidgets .QInputDialog .getMultiLineText (self , f"Edit sidecar metadata for { key } " , 'json data' , text = json .dumps (meta , indent = 2 ))
622+ if ok :
623+ try :
624+ meta_ = json .loads (text )
625+ self .output_bidsmap .dataformat (dataformat ).participant [key ]['meta' ] = meta_
626+ if meta_ != meta :
627+ self .datachanged = True
628+ except json .decoder .JSONDecodeError as jsonerror :
629+ QMessageBox .warning (self , f"Sidecar metadata parsing error" , f"{ text } \n \n Please provide valid json metadata:\n { jsonerror } " )
630+ self .edit_metadata ()
631+
632+ def fill_samples_table (self , dataformat : str ):
633+ """(Re)populate the sample table with bidsnames according to the new bidsmap data"""
561634
562635 self .datachanged = True
563636 output_bidsmap = self .output_bidsmap
564637
565- # Update the subject/session table
566- subitem = MyQTableItem ('subject' , editable = False )
567- subitem .setToolTip (get_entityhelp ('sub' ))
568- sesitem = MyQTableItem ('session' , editable = False )
569- sesitem .setToolTip (get_entityhelp ('ses' ))
570- subses_table = self .subses_table [dataformat ]
571- subses_table .setItem (0 , 0 , subitem )
572- subses_table .setItem (1 , 0 , sesitem )
573- subses_table .setItem (0 , 1 , MyQTableItem (output_bidsmap .dataformat (dataformat ).subject ))
574- subses_table .setItem (1 , 1 , MyQTableItem (output_bidsmap .dataformat (dataformat ).session ))
575-
576- # Update the run samples table
638+ # Add runs to the samples table
577639 idx = 0
578640 num_files = self .set_ordered_file_index (dataformat )
579641 samples_table = self .samples_table [dataformat ]
@@ -662,28 +724,10 @@ def set_ordered_file_index(self, dataformat: str) -> int:
662724
663725 return len (provenances )
664726
665- def subsescell2bidsmap (self , rowindex : int , colindex : int ):
666- """Subject or session value has been changed in subject-session table"""
667-
668- # Only if cell was actually clicked, update
669- dataformat = self .tabwidget .widget (self .tabwidget .currentIndex ()).objectName ()
670- if colindex == 1 and dataformat in self .dataformats :
671- key = self .subses_table [dataformat ].item (rowindex , 0 ).text ().strip ()
672- value = self .subses_table [dataformat ].item (rowindex , 1 ).text ().strip ()
673- oldvalue = getattr (self .output_bidsmap .dataformat (dataformat ), key )
674- if oldvalue is None :
675- oldvalue = ''
676-
677- # Only if cell content was changed, update
678- if key and value != oldvalue :
679- LOGGER .verbose (f"User sets { dataformat } ['{ key } '] from '{ oldvalue } ' to '{ value } '" )
680- setattr (self .output_bidsmap .dataformat (dataformat ), key , value )
681- self .update_subses_samples (dataformat )
682-
683727 def open_editwindow (self , provenance : Path = Path (), datatype : str = '' ):
684728 """Make sure that index map has been updated"""
685729
686- dataformat = self .tabwidget .widget ( self . tabwidget . currentIndex () ).objectName ()
730+ dataformat = self .tabwidget .currentWidget ( ).objectName ()
687731 if not datatype :
688732 samples_table = self .samples_table [dataformat ]
689733 clicked = self .focusWidget ()
@@ -702,7 +746,7 @@ def open_editwindow(self, provenance: Path=Path(), datatype: str=''):
702746 LOGGER .verbose (f'User is editing { provenance } ' )
703747 self .editwindow = EditWindow (runitem , self .output_bidsmap , self .template_bidsmap )
704748 self .editwindow_opened = str (provenance )
705- self .editwindow .done_edit .connect (self .update_subses_samples )
749+ self .editwindow .done_edit .connect (self .fill_samples_table )
706750 self .editwindow .finished .connect (self .release_editwindow )
707751 self .editwindow .show ()
708752 return
@@ -777,7 +821,7 @@ def options2bidsmap(self, rowindex: int, colindex: int):
777821 self .ignoredatatypes = newoptions .get ('ignoretypes' , [])
778822 self .bidsignore = newoptions .get ('bidsignore' , [])
779823 for dataformat in self .dataformats :
780- self .update_subses_samples (dataformat )
824+ self .fill_samples_table (dataformat )
781825 else :
782826 self .output_bidsmap .plugins [plugin ] = newoptions
783827
@@ -942,7 +986,7 @@ def save_options(self):
942986 def sample_doubleclicked (self , item ):
943987 """When source file is double-clicked in the samples_table, show the inspect- or edit-window"""
944988
945- dataformat = self .tabwidget .widget ( self . tabwidget . currentIndex () ).objectName ()
989+ dataformat = self .tabwidget .currentWidget ( ).objectName ()
946990 datatype = self .samples_table [dataformat ].item (item .row (), 2 ).text ()
947991 sourcefile = self .samples_table [dataformat ].item (item .row (), 5 ).text ()
948992 if item .column () == 1 :
@@ -1135,7 +1179,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11351179 layout1_ .addWidget (inspect_button , alignment = QtCore .Qt .AlignmentFlag .AlignRight )
11361180 layout1_ .addWidget (log_table_label )
11371181 layout1_ .addWidget (log_table )
1138- self .events_inbox = events_inbox = QGroupBox (f"{ self .dataformat } input" )
1182+ self .events_inbox = events_inbox = QGroupBox (f"{ self .dataformat } input data " )
11391183 events_inbox .setSizePolicy (sizepolicy )
11401184 events_inbox .setLayout (layout1_ )
11411185
@@ -1167,7 +1211,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11671211 layout2_ .addWidget (events_time_label )
11681212 layout2_ .addWidget (events_time )
11691213 layout2_ .addStretch ()
1170- self .events_editbox = events_editbox = QGroupBox (' ' )
1214+ self .events_editbox = events_editbox = QGroupBox ('Mapping ' )
11711215 events_editbox .setSizePolicy (sizepolicy )
11721216 events_editbox .setLayout (layout2_ )
11731217
@@ -1176,7 +1220,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11761220 layout3 .addWidget (done_button , alignment = QtCore .Qt .AlignmentFlag .AlignRight )
11771221 layout3 .addWidget (events_table_label )
11781222 layout3 .addWidget (events_table )
1179- self .eventsbox = eventsbox = QGroupBox ('BIDS output' )
1223+ self .eventsbox = eventsbox = QGroupBox ('BIDS output data ' )
11801224 eventsbox .setSizePolicy (sizepolicy )
11811225 eventsbox .setLayout (layout3 )
11821226
@@ -1446,7 +1490,7 @@ def run2data(self) -> tuple:
14461490 return properties_data , attributes_data , bids_data , meta_data , events_data
14471491
14481492 def properties2run (self , rowindex : int , colindex : int ):
1449- """Source attribute value has been changed"""
1493+ """Source attribute value has been changed. If OK, update the target run """
14501494
14511495 # Only if cell was actually clicked, update (i.e. not when BIDS datatype changes)
14521496 if colindex == 1 :
@@ -1470,7 +1514,7 @@ def properties2run(self, rowindex: int, colindex: int):
14701514 self .properties_table .blockSignals (False )
14711515
14721516 def attributes2run (self , rowindex : int , colindex : int ):
1473- """Source attribute value has been changed"""
1517+ """Source attribute value has been changed. If OK, update the target run """
14741518
14751519 # Only if cell was actually clicked, update (i.e. not when BIDS datatype changes)
14761520 if colindex == 1 :
@@ -1494,7 +1538,7 @@ def attributes2run(self, rowindex: int, colindex: int):
14941538 self .attributes_table .blockSignals (False )
14951539
14961540 def bids2run (self , rowindex : int , colindex : int ):
1497- """BIDS attribute value has been changed"""
1541+ """BIDS attribute value has been changed. If OK, update the target run """
14981542
14991543 # Only if cell was actually clicked, update (i.e. not when BIDS datatype changes) and store the data in the target_run
15001544 if colindex == 1 :
@@ -1533,7 +1577,7 @@ def bids2run(self, rowindex: int, colindex: int):
15331577 self .refresh_bidsname ()
15341578
15351579 def meta2run (self , rowindex : int , colindex : int ):
1536- """Meta value has been changed"""
1580+ """Meta value has been changed. If OK, update the target run """
15371581
15381582 key = self .meta_table .item (rowindex , 0 ).text ().strip ()
15391583 value = self .meta_table .item (rowindex , 1 ).text ().strip ()
@@ -1566,7 +1610,7 @@ def meta2run(self, rowindex: int, colindex: int):
15661610 self .fill_table (self .meta_table , meta_data )
15671611
15681612 def events_time2run (self , rowindex : int , colindex : int ):
1569- """Events value has been changed. Read the data from the event 'time' table"""
1613+ """Events value has been changed. Read the data from the event 'time' table and, if OK, update the target run """
15701614
15711615 # events_data['time'] = [['columns', events.timecols],
15721616 # ['units/sec', events.timeunit],
@@ -1599,7 +1643,7 @@ def events_time2run(self, rowindex: int, colindex: int):
15991643 self .fill_table (self .events_table , events_data ['table' ])
16001644
16011645 def events_rows2run (self , rowindex : int , colindex : int ):
1602- """Events value has been changed. Read the data from the event 'rows' table"""
1646+ """Events value has been changed. Read the data from the event 'rows' table and, if OK, update the target run """
16031647
16041648 # row: [[include, {column_in: regex}],
16051649 # [cast, {column_out: newvalue}]]
@@ -1627,7 +1671,7 @@ def events_rows2run(self, rowindex: int, colindex: int):
16271671 self .fill_table (self .events_rows , events_data ['rows' ])
16281672
16291673 def events_columns2run (self , rowindex : int , colindex : int ):
1630- """Events value has been changed. Read the data from the event 'columns' table"""
1674+ """Events value has been changed. Read the data from the event 'columns' table and, if OK, update the target run """
16311675
16321676 # events_data['columns'] = [[{'source1': target1}],
16331677 # [{'source2': target2}],
@@ -2172,6 +2216,25 @@ def get_suffixhelp(suffix: str) -> str:
21722216 return f"{ suffix } \n An unknown/private suffix"
21732217
21742218
2219+ def get_columnhelp (column : str ) -> str :
2220+ """
2221+ Reads the description of a matching entity=entitykey in the bidsschema.objects.columns
2222+
2223+ :param column: The column name for which the help text is obtained
2224+ :return: The obtained help text
2225+ """
2226+
2227+ if not column :
2228+ return "Please provide a column-name"
2229+
2230+ # Return the description from the entities or a default text
2231+ for _ , entity in bids .bidsschema .objects .columns .items ():
2232+ if entity .name == column :
2233+ return f"{ entity .display_name } \n { entity .description } "
2234+
2235+ return f"{ column } \n An unknown/private column name"
2236+
2237+
21752238def get_entityhelp (entitykey : str ) -> str :
21762239 """
21772240 Reads the description of a matching entity=entitykey in the schema/entities.yaml file
0 commit comments