CSVFormatDialog.cpp
Go to the documentation of this file.
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4  Sonic Visualiser
5  An audio file viewer and annotation editor.
6  Centre for Digital Music, Queen Mary, University of London.
7  This file copyright 2006-2018 Chris Cannam and QMUL.
8 
9  This program is free software; you can redistribute it and/or
10  modify it under the terms of the GNU General Public License as
11  published by the Free Software Foundation; either version 2 of the
12  License, or (at your option) any later version. See the file
13  COPYING included with this distribution for more information.
14 */
15 
16 #include "CSVFormatDialog.h"
17 
18 #include "layer/LayerFactory.h"
19 
20 #include "TextAbbrev.h"
21 
22 #include <QFrame>
23 #include <QGridLayout>
24 #include <QPushButton>
25 #include <QHBoxLayout>
26 #include <QVBoxLayout>
27 #include <QTableWidget>
28 #include <QComboBox>
29 #include <QLabel>
30 #include <QDialogButtonBox>
31 #include <QCheckBox>
32 
33 #include <iostream>
34 #include <cmath>
35 
36 #include "base/Debug.h"
37 
39  CSVFormat format,
40  int maxDisplayCols) :
41  QDialog(parent),
42  m_csvFilePath(""),
43  m_referenceSampleRate(0),
44  m_format(format),
45  m_maxDisplayCols(maxDisplayCols),
46  m_fuzzyColumn(-1)
47 {
48  init();
49 }
50 
52  QString csvFilePath,
53  sv_samplerate_t referenceSampleRate,
54  int maxDisplayCols) :
55  QDialog(parent),
56  m_csvFilePath(csvFilePath),
57  m_referenceSampleRate(referenceSampleRate),
58  m_maxDisplayCols(maxDisplayCols),
59  m_fuzzyColumn(-1)
60 {
61  m_format = CSVFormat(csvFilePath);
62  m_format.setSampleRate(referenceSampleRate);
63  init();
64 }
65 
67 {
68 }
69 
70 static int sampleRates[] = {
71  8000, 11025, 12000, 22050, 24000, 32000,
72  44100, 48000, 88200, 96000, 176400, 192000
73 };
74 
75 void
77 {
78  setModal(true);
79  setWindowTitle(tr("Select Data Format"));
80 
81  m_tabText = tr("<tab>");
82  m_whitespaceText = tr("<whitespace>");
83 
84  QGridLayout *layout = new QGridLayout;
85 
86  int row = 0;
87 
88  layout->addWidget
89  (new QLabel(tr("Please select the correct data format for this file.")),
90  row++, 0, 1, 4);
91 
92  m_exampleFrame = nullptr;
93  m_exampleFrameRow = row++;
94 
95  std::set<QChar> plausible = m_format.getPlausibleSeparators();
96  SVDEBUG << "Have " << plausible.size() << " plausible separator(s)" << endl;
97 
98  if (m_csvFilePath != "" && plausible.size() > 1) {
99  // can only update when separator changed if we still have a
100  // file to refer to
101  layout->addWidget(new QLabel(tr("Column separator:")), row, 0);
102  m_separatorCombo = new QComboBox;
103  for (QChar c: plausible) {
104  if (c == '\t') {
105  m_separatorCombo->addItem(m_tabText);
106  } else if (c == ' ') {
108  } else {
109  m_separatorCombo->addItem(QString(c));
110  }
111  if (c == m_format.getSeparator()) {
112  m_separatorCombo->setCurrentIndex(m_separatorCombo->count()-1);
113  }
114  }
115  m_separatorCombo->setEditable(false);
116 
117  layout->addWidget(m_separatorCombo, row++, 1);
118  connect(m_separatorCombo, SIGNAL(activated(QString)),
119  this, SLOT(separatorChanged(QString)));
120 
121  } else {
122  m_separatorCombo = nullptr;
123  }
124 
125  layout->addWidget(new QLabel(tr("First row contains column headings:")), row, 0);
126  m_headerCheckBox = new QCheckBox;
127  m_headerCheckBox->setChecked
128  (m_format.getHeaderStatus() == CSVFormat::HeaderPresent);
129  layout->addWidget(m_headerCheckBox, row++, 1);
130  connect(m_headerCheckBox, SIGNAL(toggled(bool)),
131  this, SLOT(headerChanged(bool)));
132 
133  layout->addWidget(new QLabel(tr("Timing is specified:")), row, 0);
134 
135  m_timingTypeCombo = new QComboBox;
136 
137  m_timingLabels = {
138  { TimingExplicitSeconds, tr("Explicitly, in seconds") },
139  { TimingExplicitMsec, tr("Explicitly, in milliseconds") },
140  { TimingExplicitSamples, tr("Explicitly, in audio sample frames") },
141  { TimingImplicit, tr("Implicitly: rows are equally spaced in time") }
142  };
143 
144  for (auto &l: m_timingLabels) {
145  m_timingTypeCombo->addItem(l.second);
146  }
147 
148  layout->addWidget(m_timingTypeCombo, row++, 1, 1, 2);
149 
150  connect(m_timingTypeCombo, SIGNAL(activated(int)),
151  this, SLOT(timingTypeChanged(int)));
152 
153  m_sampleRateLabel = new QLabel(tr("Audio sample rate (Hz):"));
154  layout->addWidget(m_sampleRateLabel, row, 0);
155 
156  m_sampleRateCombo = new QComboBox;
157  for (int i = 0; i < int(sizeof(sampleRates) / sizeof(sampleRates[0])); ++i) {
158  m_sampleRateCombo->addItem(QString("%1").arg(sampleRates[i]));
159  }
160  m_sampleRateCombo->setEditable(true);
161 
162  layout->addWidget(m_sampleRateCombo, row++, 1);
163  connect(m_sampleRateCombo, SIGNAL(activated(QString)),
164  this, SLOT(sampleRateChanged(QString)));
165  connect(m_sampleRateCombo, SIGNAL(editTextChanged(QString)),
166  this, SLOT(sampleRateChanged(QString)));
167 
168  m_windowSizeLabel = new QLabel(tr("Frame increment between rows:"));
169  layout->addWidget(m_windowSizeLabel, row, 0);
170 
171  m_windowSizeCombo = new QComboBox;
172  for (int i = 0; i <= 16; ++i) {
173  int value = 1 << i;
174  m_windowSizeCombo->addItem(QString("%1").arg(value));
175  }
176  m_windowSizeCombo->setEditable(true);
177 
178  layout->addWidget(m_windowSizeCombo, row++, 1);
179  connect(m_windowSizeCombo, SIGNAL(activated(QString)),
180  this, SLOT(windowSizeChanged(QString)));
181  connect(m_windowSizeCombo, SIGNAL(editTextChanged(QString)),
182  this, SLOT(windowSizeChanged(QString)));
183 
184  m_modelLabel = new QLabel;
185  QFont f(m_modelLabel->font());
186  f.setItalic(true);
187  m_modelLabel->setFont(f);
188  layout->addWidget(m_modelLabel, row++, 0, 1, 4);
189 
190  QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok |
191  QDialogButtonBox::Cancel);
192  layout->addWidget(bb, row++, 0, 1, 4);
193  connect(bb, SIGNAL(accepted()), this, SLOT(accept()));
194  connect(bb, SIGNAL(rejected()), this, SLOT(reject()));
195 
196  setLayout(layout);
197 
198  repopulate();
199 }
200 
201 void
203 {
204  SVCERR << "CSVFormatDialog::repopulate()" << endl;
205 
206  QGridLayout *layout = qobject_cast<QGridLayout *>(this->layout());
207 
208  QFrame *exampleFrame = new QFrame;
209  exampleFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
210  exampleFrame->setLineWidth(2);
211  QGridLayout *exampleLayout = new QGridLayout;
212  exampleLayout->setSpacing(4);
213  exampleFrame->setLayout(exampleLayout);
214 
215  QPalette palette = exampleFrame->palette();
216  palette.setColor(QPalette::Window, palette.color(QPalette::Base));
217  exampleFrame->setPalette(palette);
218 
219  QFont fp;
220  fp.setPointSize(int(floor(fp.pointSize() * 0.9)));
221 
222  QFont fpi(fp);
223  fpi.setItalic(true);
224 
225  int columns = m_format.getColumnCount();
226  QList<QStringList> example = m_format.getExample();
227 
228  m_columnPurposeCombos.clear();
229 
230  for (int i = 0; i < columns; ++i) {
231 
232  QComboBox *cpc = new QComboBox;
233  m_columnPurposeCombos.push_back(cpc);
234  exampleLayout->addWidget(cpc, 0, i);
235  connect(cpc, SIGNAL(activated(int)), this, SLOT(columnPurposeChanged(int)));
236 
237  if (i == m_maxDisplayCols && columns > i + 2) {
238  m_fuzzyColumn = i;
239 
240  cpc->addItem(tr("<ignore>"));
241  cpc->addItem(tr("Values"));
242  cpc->setCurrentIndex
243  (m_format.getColumnPurpose(i-1) ==
244  CSVFormat::ColumnUnknown ? 0 : 1);
245 
246  exampleLayout->addWidget
247  (new QLabel(tr("(%1 more)").arg(columns - i)), 1, i);
248  break;
249  }
250 
251  // NB must be in the same order as the CSVFormat::ColumnPurpose enum
252  cpc->addItem(tr("<ignore>")); // ColumnUnknown
253  cpc->addItem(tr("Time")); // ColumnStartTime
254  cpc->addItem(tr("End time")); // ColumnEndTime
255  cpc->addItem(tr("Duration")); // ColumnDuration
256  cpc->addItem(tr("Value")); // ColumnValue
257  cpc->addItem(tr("Pitch")); // ColumnPitch
258  cpc->addItem(tr("Label")); // ColumnLabel
259  cpc->setCurrentIndex(int(m_format.getColumnPurpose(i)));
260 
261  for (int j = 0; j < example.size() && j < 6; ++j) {
262  if (i >= example[j].size()) {
263  continue;
264  }
265  QLabel *label = new QLabel;
266  label->setTextFormat(Qt::PlainText);
267  QString text = TextAbbrev::abbreviate(example[j][i], 35);
268  label->setText(text);
269  if (j == 0 &&
270  m_format.getHeaderStatus() == CSVFormat::HeaderPresent) {
271  label->setFont(fpi);
272  } else {
273  label->setFont(fp);
274  }
275  label->setPalette(palette);
276  label->setIndent(8);
277  exampleLayout->addWidget(label, j+1, i);
278  }
279  }
280 
281  if (m_exampleFrame) {
282  delete m_exampleFrame;
283  }
284  m_exampleFrame = exampleFrame;
285 
286  layout->addWidget(exampleFrame, m_exampleFrameRow, 0, 1, 4);
287  layout->setColumnStretch(3, 10);
288  layout->setRowStretch(m_exampleFrameRow, 10);
289 
291  if (m_format.getTimingType() == CSVFormat::ExplicitTiming) {
292  switch (m_format.getTimeUnits()) {
293  case CSVFormat::TimeSeconds:
295  case CSVFormat::TimeMilliseconds:
297  case CSVFormat::TimeAudioFrames:
299  case CSVFormat::TimeWindows:
301  }
302  }
303  m_timingTypeCombo->setCurrentIndex(int(m_initialTimingOption));
304 
305  for (int i = 0; i < int(sizeof(sampleRates) / sizeof(sampleRates[0])); ++i) {
306  if (sampleRates[i] == m_format.getSampleRate()) {
307  m_sampleRateCombo->setCurrentIndex(i);
308  }
309  }
310 
311  for (int i = 0; i <= 16; ++i) {
312  int value = 1 << i;
313  if (value == int(m_format.getWindowSize())) {
314  m_windowSizeCombo->setCurrentIndex(i);
315  }
316  }
317 
318  timingTypeChanged(m_timingTypeCombo->currentIndex());
319 }
320 
321 CSVFormat
323 {
324  return m_format;
325 }
326 
327 void
329 {
330  if (!m_modelLabel) {
331  return;
332  }
333 
335 
336  QString s;
337  switch (m_format.getModelType()) {
338  case CSVFormat::OneDimensionalModel:
340  break;
341  case CSVFormat::TwoDimensionalModel:
343  break;
344  case CSVFormat::TwoDimensionalModelWithDuration:
346  break;
347  case CSVFormat::TwoDimensionalModelWithDurationAndPitch:
349  break;
350  case CSVFormat::TwoDimensionalModelWithDurationAndExtent:
352  break;
353  case CSVFormat::ThreeDimensionalModel:
355  break;
356  case CSVFormat::WaveFileModel:
358  break;
359  }
360 
361  m_modelLabel->setText("\n" + tr("Data will be displayed in a %1 layer.")
362  .arg(s));
363 }
364 
365 void
367 {
368  // First check if we already have any. NB there may be fewer than
369  // m_format.getColumnCount() elements in m_columnPurposeCombos
370  // (because of the fuzzy column behaviour). Note also that the
371  // fuzzy column (which is the one just showing how many more
372  // columns there are) has a different combo with only two items
373  // (ignore or Values)
374  for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
375  if (i == m_fuzzyColumn) continue;
376  QComboBox *cb = m_columnPurposeCombos[i];
377  if (cb->currentIndex() == int(CSVFormat::ColumnStartTime)) {
378  return;
379  }
380  }
381  // and if not, select one
382  for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
383  if (i == m_fuzzyColumn) continue;
384  QComboBox *cb = m_columnPurposeCombos[i];
385  if (cb->currentIndex() == int(CSVFormat::ColumnValue)) {
386  cb->setCurrentIndex(int(CSVFormat::ColumnStartTime));
387  return;
388  }
389  }
390 }
391 
392 void
394 {
395  // NB there may be fewer than m_format.getColumnCount() elements
396  // in m_columnPurposeCombos (because of the fuzzy column
397  // behaviour)
398  for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
399  if (i == m_fuzzyColumn) continue;
400  QComboBox *cb = m_columnPurposeCombos[i];
401  if (cb->currentIndex() == int(CSVFormat::ColumnStartTime)) {
402  cb->setCurrentIndex(int(CSVFormat::ColumnValue));
403  }
404  }
405 }
406 
407 void
409 {
410  bool wantRate = (m_format.getTimingType() == CSVFormat::ImplicitTiming ||
411  m_format.getTimeUnits() == CSVFormat::TimeAudioFrames);
412  bool wantWindow = (m_format.getTimingType() == CSVFormat::ImplicitTiming);
413 
414  m_sampleRateCombo->setEnabled(wantRate);
415  m_sampleRateLabel->setEnabled(wantRate);
416 
417  m_windowSizeCombo->setEnabled(wantWindow);
418  m_windowSizeLabel->setEnabled(wantWindow);
419 }
420 
421 void
423 {
424  m_format.setHeaderStatus(header ?
425  CSVFormat::HeaderPresent :
426  CSVFormat::HeaderAbsent);
427  m_format.guessFormatFor(m_csvFilePath);
428 
429  repopulate();
430 }
431 
432 void
434 {
435  if (sep == "" || m_csvFilePath == "") {
436  return;
437  }
438 
439  if (sep == m_tabText) {
440  sep = "\t";
441  } else if (sep == m_whitespaceText) {
442  sep = " ";
443  }
444 
445  m_format.setSeparator(sep[0]);
446  m_format.guessFormatFor(m_csvFilePath);
447 
448  repopulate();
449 }
450 
451 void
453 {
454  // Update any column purpose combos
455  if (TimingOption(type) == TimingImplicit) {
457  } else {
459  }
462 }
463 
464 void
466 {
467  bool ok = false;
468  int sampleRate = rateString.toInt(&ok);
469  if (ok) m_format.setSampleRate(sampleRate);
470 }
471 
472 void
474 {
475  bool ok = false;
476  int size = sizeString.toInt(&ok);
477  if (ok) m_format.setWindowSize(size);
478 }
479 
480 void
482 {
483  QObject *o = sender();
484  QComboBox *cb = qobject_cast<QComboBox *>(o);
485  if (!cb) return;
486 
487  // Ensure a consistent set of column purposes, in case of a
488  // situation where some combinations are contradictory. Only
489  // updates the UI, does not update the stored format record from
490  // the UI - that's the job of updateFormatFromDialog
491 
492  CSVFormat::ColumnPurpose purpose = (CSVFormat::ColumnPurpose)p;
493 
494  bool haveStartTime = false; // so as to update timing type combo appropriately
495 
496  // Ensure the column purpose combos are consistent with one
497  // another, without reference to m_format (which we'll update
498  // separately)
499 
500  for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
501 
502  // The fuzzy column combo only has the entries <ignore> or
503  // Values, so it can't affect the timing type and none of this
504  // logic affects it
505  if (i == m_fuzzyColumn) continue;
506 
507  QComboBox *thisCombo = m_columnPurposeCombos[i];
508 
509  CSVFormat::ColumnPurpose cp = (CSVFormat::ColumnPurpose)
510  (thisCombo->currentIndex());
511  bool thisChanged = (cb == thisCombo);
512 
513  if (!thisChanged) {
514 
515  // We can only have one ColumnStartTime column, and only
516  // one of either ColumnDuration or ColumnEndTime
517 
518  if (purpose == CSVFormat::ColumnStartTime) {
519  if (cp == purpose) {
520  cp = CSVFormat::ColumnValue;
521  }
522  } else if (purpose == CSVFormat::ColumnDuration ||
523  purpose == CSVFormat::ColumnEndTime) {
524  if (cp == CSVFormat::ColumnDuration ||
525  cp == CSVFormat::ColumnEndTime) {
526  cp = CSVFormat::ColumnValue;
527  }
528  }
529 
530  // And we can only have one label
531  if (purpose == CSVFormat::ColumnLabel) {
532  if (cp == purpose) {
533  cp = CSVFormat::ColumnUnknown;
534  }
535  }
536 
537  if (cp == CSVFormat::ColumnStartTime) {
538  haveStartTime = true;
539  }
540 
541  thisCombo->setCurrentIndex(int(cp));
542 
543  } else {
544  if (purpose == CSVFormat::ColumnStartTime) {
545  haveStartTime = true;
546  }
547  }
548  }
549 
550  if (!haveStartTime) {
551  m_timingTypeCombo->setCurrentIndex(int(TimingImplicit));
552  } else if (m_timingTypeCombo->currentIndex() == int(TimingImplicit)) {
554  m_timingTypeCombo->setCurrentIndex(TimingExplicitSeconds);
555  } else {
556  m_timingTypeCombo->setCurrentIndex(m_initialTimingOption);
557  }
558  }
559 
562 }
563 
564 void
566 {
567  switch (TimingOption(m_timingTypeCombo->currentIndex())) {
568 
570  m_format.setTimingType(CSVFormat::ExplicitTiming);
571  m_format.setTimeUnits(CSVFormat::TimeSeconds);
572  break;
573 
574  case TimingExplicitMsec:
575  m_format.setTimingType(CSVFormat::ExplicitTiming);
576  m_format.setTimeUnits(CSVFormat::TimeMilliseconds);
577  break;
578 
580  m_format.setTimingType(CSVFormat::ExplicitTiming);
581  m_format.setTimeUnits(CSVFormat::TimeAudioFrames);
582  break;
583 
584  case TimingImplicit:
585  m_format.setTimingType(CSVFormat::ImplicitTiming);
586  m_format.setTimeUnits(CSVFormat::TimeWindows);
587  break;
588  }
589 
590  bool haveStartTime = false;
591  bool haveDuration = false;
592  bool havePitch = false;
593  int valueCount = 0;
594 
595  for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
596 
597  QComboBox *thisCombo = m_columnPurposeCombos[i];
598 
599  CSVFormat::ColumnPurpose purpose =
600  (CSVFormat::ColumnPurpose) (thisCombo->currentIndex());
601 
602  if (i == m_fuzzyColumn) {
603  for (int j = i; j < m_format.getColumnCount(); ++j) {
604  if (purpose == CSVFormat::ColumnUnknown) {
605  m_format.setColumnPurpose(j, CSVFormat::ColumnUnknown);
606  } else { // Value
607  m_format.setColumnPurpose(j, CSVFormat::ColumnValue);
608  ++valueCount;
609  }
610  }
611  } else {
612 
613  if (purpose == CSVFormat::ColumnStartTime) {
614  haveStartTime = true;
615  }
616  if (purpose == CSVFormat::ColumnEndTime ||
617  purpose == CSVFormat::ColumnDuration) {
618  haveDuration = true;
619  }
620  if (purpose == CSVFormat::ColumnPitch) {
621  havePitch = true;
622  }
623  if (purpose == CSVFormat::ColumnValue) {
624  ++valueCount;
625  }
626 
627  m_format.setColumnPurpose(i, purpose);
628  }
629  }
630 
631  if (haveStartTime && haveDuration) {
632  if (havePitch) {
633  m_format.setModelType(CSVFormat::TwoDimensionalModelWithDurationAndPitch);
634  } else if (valueCount == 2) {
635  m_format.setModelType(CSVFormat::TwoDimensionalModelWithDurationAndExtent);
636  } else {
637  m_format.setModelType(CSVFormat::TwoDimensionalModelWithDuration);
638  }
639  } else {
640  if (valueCount > 1) {
641  m_format.setModelType(CSVFormat::ThreeDimensionalModel);
642  } else if (valueCount > 0) {
643  m_format.setModelType(CSVFormat::TwoDimensionalModel);
644  } else {
645  m_format.setModelType(CSVFormat::OneDimensionalModel);
646  }
647  }
648 
650 }
651 
652 
653 
void windowSizeChanged(QString)
static LayerFactory * getInstance()
void timingTypeChanged(int type)
void headerChanged(bool)
QFrame * m_exampleFrame
static QString abbreviate(QString text, int maxLength, Policy policy=ElideEnd, bool fuzzy=true, QString ellipsis="")
Abbreviate the given text to the given maximum length (including ellipsis), using the given abbreviat...
Definition: TextAbbrev.cpp:79
QLabel * m_modelLabel
TimingOption m_initialTimingOption
sv_samplerate_t m_referenceSampleRate
QComboBox * m_timingTypeCombo
QString m_whitespaceText
CSVFormat m_format
QString getLayerPresentationName(LayerType type)
void columnPurposeChanged(int purpose)
CSVFormat getFormat() const
QLabel * m_sampleRateLabel
QComboBox * m_sampleRateCombo
QComboBox * m_windowSizeCombo
void sampleRateChanged(QString)
static int sampleRates[]
CSVFormatDialog(QWidget *parent, CSVFormat initialFormat, int maxDisplayCols)
QList< QComboBox * > m_columnPurposeCombos
QLabel * m_windowSizeLabel
std::map< TimingOption, QString > m_timingLabels
QCheckBox * m_headerCheckBox
QComboBox * m_separatorCombo
void separatorChanged(QString)