Skip to content

Commit c838794

Browse files
committed
Unify legacy / BIDS template input
1 parent f163f46 commit c838794

5 files changed

Lines changed: 131 additions & 115 deletions

File tree

LabRecorder_BIDS.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; BIDS config, files are saved in the specified StudyRoot
2+
StudyRoot=C:/Recordings/CurrentStudy

LabRecorder_Legacy.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Legacy config, the StudyRoot and TemplatePath are extracted automatically
2+
; from the StorageLocation
3+
StorageLocation = "C:/Recordings/CurrentStudy/expi_%n/blockvar_%b.xdf"

mainwindow.cpp

Lines changed: 93 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,25 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file)
4242
});
4343

4444
// Wheenver lineEdit_template is changed, print the final result.
45-
connect(ui->lineEdit_template, &QLineEdit::textChanged, this, &MainWindow::printReplacedFilename);
45+
connect(
46+
ui->lineEdit_template, &QLineEdit::textChanged, this, &MainWindow::printReplacedFilename);
4647
auto spinchanged = static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged);
4748
connect(ui->experimentNumberSpin, spinchanged, this, &MainWindow::printReplacedFilename);
48-
connect(ui->spinBox_run, spinchanged, this, &MainWindow::printReplacedFilename);
4949

5050
// Signals for builder-related edits -> buildFilename
5151
connect(ui->rootBrowseButton, &QPushButton::clicked, [this]() {
52-
this->ui->rootEdit->setText(QFileDialog::getExistingDirectory(this, "Study root folder..."));
52+
this->ui->rootEdit->setText(
53+
QFileDialog::getExistingDirectory(this, "Study root folder..."));
5354
this->buildFilename();
5455
});
5556
connect(ui->rootEdit, &QLineEdit::editingFinished, this, &MainWindow::buildFilename);
56-
connect(ui->lineEdit_participant, &QLineEdit::editingFinished, this, &MainWindow::buildFilename);
57+
connect(
58+
ui->lineEdit_participant, &QLineEdit::editingFinished, this, &MainWindow::buildFilename);
5759
connect(ui->lineEdit_session, &QLineEdit::editingFinished, this, &MainWindow::buildFilename);
5860
connect(ui->lineEdit_acq, &QLineEdit::editingFinished, this, &MainWindow::buildFilename);
5961
connect(ui->input_blocktask, &QComboBox::currentTextChanged, this, &MainWindow::buildFilename);
60-
connect(ui->check_bids, &QCheckBox::toggled, [this](bool checked){
61-
auto& box = *ui->lineEdit_template;
62+
connect(ui->check_bids, &QCheckBox::toggled, [this](bool checked) {
63+
auto &box = *ui->lineEdit_template;
6264
box.setReadOnly(checked);
6365
if (checked) {
6466
legacyTemplate = box.text();
@@ -124,7 +126,7 @@ void MainWindow::load_config(QString filename) {
124126
// ----------------------------
125127
// online sync streams
126128
// ----------------------------
127-
QStringList onlineSyncStreams = pt.value("OnlineSync", "").toStringList();
129+
QStringList onlineSyncStreams = pt.value("OnlineSync", QStringList()).toStringList();
128130
for (QString &oss : onlineSyncStreams) {
129131
QStringList words = oss.split(' ', QString::SkipEmptyParts);
130132
// The first two words ("StreamName (PC)") are the stream identifier
@@ -154,18 +156,53 @@ void MainWindow::load_config(QString filename) {
154156
ui->input_blocktask->clear();
155157
ui->input_blocktask->insertItems(0, taskNames);
156158

159+
// StorageLocation
160+
QString studyRoot;
161+
legacyTemplate.clear();
157162
// StudyRoot
158-
if (pt.contains("StudyRoot")) {
159-
ui->rootEdit->setText(pt.value("StudyRoot").toString());
163+
if (pt.contains("StudyRoot")) { studyRoot = pt.value("StudyRoot").toString(); }
164+
165+
if (pt.contains("StorageLocation")) {
166+
if (!studyRoot.isEmpty())
167+
throw std::runtime_error("Both StudyRoot and StorageLocation specified");
168+
if (pt.contains("PathTemplate"))
169+
throw std::runtime_error("Both StorageLocation and PathTemplate specified");
170+
171+
QString str_path = pt.value("StorageLocation").toString();
172+
QString path_root;
173+
auto index = str_path.indexOf('%');
174+
if (index != -1) {
175+
// When a % is encountered, the studyroot gets set to the
176+
// longest path before the placeholder, e.g.
177+
// foo/bar/baz%a/untitled.xdf gets split into
178+
// foo/bar and baz%a/untitled.xdf
179+
path_root = str_path.left(index);
180+
} else
181+
// Otherwise, it's split into folder and constant filename
182+
path_root = str_path;
183+
path_root = QFileInfo(path_root).path();
184+
legacyTemplate = str_path.remove(0, path_root.length() + 1);
185+
// absolute path, nothing to be done
186+
studyRoot = QFileInfo(path_root).absolutePath();
187+
ui->lineEdit_template->setText(legacyTemplate);
188+
}
189+
if (pt.contains("PathTemplate")) legacyTemplate = pt.value("PathTemplate").toString();
190+
191+
if (studyRoot.isEmpty())
192+
studyRoot = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) +
193+
QDir::separator() + "CurrentStudy";
194+
ui->rootEdit->setText(studyRoot);
195+
196+
if (legacyTemplate.isEmpty()) {
197+
ui->check_bids->setChecked(true);
198+
// Legacy takes the form path/to/study/exp%n/%b.xdf
199+
legacyTemplate = "exp%n/block_%b.xdf";
160200
} else {
161-
ui->rootEdit->setText(
162-
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
201+
ui->check_bids->setChecked(false);
202+
ui->lineEdit_template->setText(legacyTemplate);
163203
}
164204

165-
// StorageLocation
166-
QString str_path = pt.value("StorageLocation", "").toString();
167-
ui->lineEdit_template->setText(str_path);
168-
if (str_path.length() == 0) { buildFilename(); }
205+
buildFilename();
169206

170207
// Check the wild-card-replaced filename to see if it exists already.
171208
// If it does then increment the exp number.
@@ -175,18 +212,20 @@ void MainWindow::load_config(QString filename) {
175212
if (recFilename.contains(QStringLiteral("%n"))) {
176213
for (int i = 1; i < 1001; i++) {
177214
ui->experimentNumberSpin->setValue(i);
178-
if (!QDir(replaceFilename(recFilename)).exists()) { break; }
215+
if (!QFileInfo::exists(replaceFilename(recFilename))) break;
179216
}
180217
}
181-
218+
182219
} catch (std::exception &e) { qWarning() << "Problem parsing config file: " << e.what(); }
183220
// std::cout << "refreshing streams ..." <<std::endl;
184221
refreshStreams();
185222
}
186223

187224
void MainWindow::save_config(QString filename) {
188225
QSettings settings(filename, QSettings::Format::IniFormat);
189-
settings.setValue("StorageLocation", ui->lineEdit_template->text());
226+
settings.setValue("StudyRoot", ui->lineEdit_template->text());
227+
if (!ui->check_bids->isChecked())
228+
settings.setValue("PathTemplate", ui->lineEdit_template->text());
190229
qInfo() << requiredStreams;
191230
settings.setValue("RequiredStreams", requiredStreams);
192231
// Stub.
@@ -261,12 +300,12 @@ void MainWindow::startRecording() {
261300
if (msgBox.exec() != QMessageBox::Yes) return;
262301
}
263302

264-
// Handle wildcards in filename.
265303
QString recFilename = replaceFilename(ui->lineEdit_template->text());
266304
if (recFilename.isEmpty()) {
267305
QMessageBox::critical(this, "Filename empty", "Can not record without a file name");
268306
return;
269307
}
308+
recFilename.prepend(ui->rootEdit->text() + QDir::separator());
270309

271310
QFileInfo recFileInfo(recFilename);
272311
if (recFileInfo.exists()) {
@@ -275,8 +314,8 @@ void MainWindow::startRecording() {
275314
this, "Error", "Recording path already exists and is a directory");
276315
return;
277316
}
278-
QString rename_to = recFileInfo.absolutePath() + '/' + recFileInfo.baseName() +
279-
"_old%1." + recFileInfo.suffix();
317+
QString rename_to = recFileInfo.absolutePath() + QDir::separator() +
318+
recFileInfo.baseName() + "_old%1." + recFileInfo.suffix();
280319
// search for highest _oldN
281320
int i = 1;
282321
while (QFileInfo::exists(rename_to.arg(i))) i++;
@@ -351,72 +390,44 @@ void MainWindow::selectNoStreams() {
351390
}
352391
}
353392

354-
void MainWindow::buildFilename() {
355-
// This function is only called when a widget within Location Builder is activated.
393+
void MainWindow::buildBidsTemplate() {
394+
// path/to/CurrentStudy/sub-%p/ses-%s/eeg/sub-%p_ses-%s_task-%b[_acq-%a]_run-%r_eeg.xdf
356395

357-
// Fill root folder with default if needed.
358-
if (ui->rootEdit->text().isEmpty()) {
359-
QStringList fileparts =
360-
QStringList(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation))
361-
<< "CurrentStudy";
362-
ui->rootEdit->setText(QDir::toNativeSeparators(fileparts.join(QDir::separator())));
396+
// Make sure the BIDS required fields are full.
397+
if (ui->lineEdit_participant->text().isEmpty()) { ui->lineEdit_participant->setText("P001"); }
398+
if (ui->lineEdit_session->text().isEmpty()) { ui->lineEdit_session->setText("S001"); }
399+
if (ui->input_blocktask->currentText().isEmpty()) {
400+
ui->input_blocktask->setCurrentText("Default");
363401
}
364402

403+
// Folder hierarchy
404+
QStringList fileparts{"sub-%p", "ses-%s", "eeg"};
405+
406+
// filename
407+
QString fname = "sub-%p_ses-%s_task-%b";
408+
if (!ui->lineEdit_acq->text().isEmpty()) { fname.append("_acq-%a"); }
409+
fname.append("_run-%r_eeg.xdf");
410+
fileparts << fname;
411+
ui->lineEdit_template->setText(fileparts.join(QDir::separator()));
412+
}
413+
414+
void MainWindow::buildFilename() {
415+
// This function is only called when a widget within Location Builder is activated.
416+
365417
// Build the file location in parts, starting with the root folder.
366-
QStringList fileparts = QStringList(ui->rootEdit->text());
367-
bool hasTasks = ui->input_blocktask->currentText().isEmpty();
368-
369-
bool use_bids = ui->check_bids->isChecked();
370-
if (use_bids) {
371-
// path/to/CurrentStudy/sub-%p/ses-%s/eeg/sub-%p_ses-%s_task-%b[_acq-%a]_run-%r_eeg.xdf
372-
373-
// Make sure the BIDS required fields are full.
374-
if (ui->lineEdit_participant->text().isEmpty()) { ui->lineEdit_participant->setText("P001"); }
375-
if (ui->lineEdit_session->text().isEmpty()) { ui->lineEdit_session->setText("S001"); }
376-
if (ui->input_blocktask->currentText().isEmpty()) { ui->input_blocktask->setCurrentText("Default"); }
377-
378-
// Folder hierarchy
379-
fileparts << "sub-%p"
380-
<< "ses-%s"
381-
<< "eeg";
382-
383-
// filename
384-
QString fname = "sub-%p_ses-%s_task-%b";
385-
if (!ui->lineEdit_acq->text().isEmpty()) { fname.append("_acq-%a"); }
386-
fname.append("_run-%r_eeg.xdf");
387-
fileparts << fname;
388-
} else // !use_bids == use legacy
389-
{
390-
// Legacy takes the form path/to/study/exp%n/%b.xdf
391-
fileparts << "exp%n";
392-
393-
QString fname = "";
394-
if (hasTasks) {
395-
fname.append("%b.xdf");
396-
} else {
397-
fname.append("untitled.xdf");
398-
}
399-
fileparts << fname;
400-
}
401-
QString recFilename = QDir::toNativeSeparators(fileparts.join(QDir::separator()));
418+
if (ui->check_bids->isChecked()) buildBidsTemplate();
419+
QString tpl = ui->lineEdit_template->text();
402420

403421
// Auto-increment Spin/Run Number if necessary.
404-
if (recFilename.contains(QStringLiteral("%n"))) {
422+
if (tpl.contains("%n") || tpl.contains("%r")) {
405423
for (int i = 1; i < 1001; i++) {
406424
ui->experimentNumberSpin->setValue(i);
407-
if (!QDir(replaceFilename(recFilename)).exists()) { break; }
408-
}
409-
}
410-
if (recFilename.contains(QStringLiteral("%r"))) {
411-
for (int i = 1; i < 1001; i++) {
412-
ui->spinBox_run->setValue(i);
413-
if (!QDir(replaceFilename(recFilename)).exists()) { break; }
425+
if (!QFileInfo::exists(replaceFilename(tpl))) break;
414426
}
415427
}
416428
// Sometimes lineEdit_template doesn't change so printReplacedFilename isn't triggered.
417429
// So trigger manually.
418-
if (ui->lineEdit_template->text() == recFilename) { printReplacedFilename(); }
419-
ui->lineEdit_template->setText(recFilename);
430+
printReplacedFilename();
420431
}
421432

422433
QString MainWindow::replaceFilename(QString fullfile) const {
@@ -426,9 +437,10 @@ QString MainWindow::replaceFilename(QString fullfile) const {
426437
// Legacy takes the form path/to/study/exp%n/%b.xdf
427438
// Where %n will be replaced by the contents of the experimentNumberSpin widget
428439
// and %b will be replaced by the contents of the blockList widget.
429-
fullfile.replace("%n", QString("%1").arg(ui->experimentNumberSpin->value(), 3, 10, QChar('0')));
440+
QString run = QString("%1").arg(ui->experimentNumberSpin->value(), 3, 10, QChar('0'));
441+
fullfile.replace("%n", run);
430442
fullfile.replace("%b", ui->input_blocktask->currentText());
431-
443+
432444
// BIDS
433445
// See https://docs.google.com/document/d/1ArMZ9Y_quTKXC-jNXZksnedK2VHHoKP3HCeO5HPcgLE/
434446
// path/to/study/sub-<participant_label>/ses-<session_label>/eeg/sub-<participant_label>_ses-<session_label>_task-<task_label>[_acq-<acq_label>]_run-<run_index>_eeg.xdf
@@ -437,7 +449,7 @@ QString MainWindow::replaceFilename(QString fullfile) const {
437449
fullfile.replace("%p", ui->lineEdit_participant->text());
438450
fullfile.replace("%s", ui->lineEdit_session->text());
439451
fullfile.replace("%a", ui->lineEdit_acq->text());
440-
fullfile.replace("%r", QString("%1").arg(ui->spinBox_run->value(), 3, 10, QChar('0')));
452+
fullfile.replace("%r", run);
441453

442454
return fullfile;
443455
}
@@ -464,6 +476,7 @@ QString MainWindow::find_config_file(const char *filename) {
464476
}
465477
QFileInfo exeInfo(QCoreApplication::applicationFilePath());
466478
QString defaultCfgFilename(exeInfo.completeBaseName() + ".cfg");
479+
qInfo() << defaultCfgFilename;
467480
QStringList cfgpaths;
468481
cfgpaths << QDir::currentPath()
469482
<< QStandardPaths::standardLocations(QStandardPaths::ConfigLocation) << exeInfo.path();
@@ -477,5 +490,6 @@ QString MainWindow::find_config_file(const char *filename) {
477490
}
478491

479492
void MainWindow::printReplacedFilename() {
480-
ui->locationLabel->setText(replaceFilename(ui->lineEdit_template->text()));
493+
ui->locationLabel->setText(
494+
ui->rootEdit->text() + '\n' + replaceFilename(ui->lineEdit_template->text()));
481495
}

mainwindow.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ private slots:
4242
void selectAllStreams();
4343
void selectNoStreams();
4444
void buildFilename();
45+
void buildBidsTemplate();
4546
void printReplacedFilename();
4647

4748
private:

0 commit comments

Comments
 (0)