InteractiveFileFinder.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 2007 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 "InteractiveFileFinder.h"
17 #include "data/fileio/FileSource.h"
18 #include "data/fileio/AudioFileReaderFactory.h"
19 #include "data/fileio/DataFileReaderFactory.h"
20 #include "rdf/RDFImporter.h"
21 #include "rdf/RDFExporter.h"
22 #include "system/System.h"
23 
24 #include <QFileInfo>
25 #include <QMessageBox>
26 #include <QFileDialog>
27 #include <QInputDialog>
28 #include <QImageReader>
29 #include <QSettings>
30 
31 #include <iostream>
32 
35 
37  m_sessionExtension("sv"),
38  m_lastLocatedLocation(""),
39  m_parent(nullptr)
40 {
41  FileFinder::registerFileFinder(this);
42 }
43 
45 {
46 }
47 
48 void
50 {
51  getInstance()->m_parent = parent;
52 }
53 
54 void
56 {
57  m_sessionExtension = extension;
58 }
59 
60 QString
62  QString fallbackLocation)
63 {
64  QStringList names = getOpenFileNames(type,
65  fallbackLocation,
66  false);
67  if (names.empty()) return "";
68  else return names[0];
69 }
70 
71 QStringList
73  QString fallbackLocation)
74 {
75  return getOpenFileNames(type,
76  fallbackLocation,
77  true);
78 }
79 
80 QStringList
82  QString fallbackLocation,
83  bool multiple)
84 {
85  QString settingsKeyStub;
86  QString lastPath = fallbackLocation;
87 
88  QString title;
89  if (multiple) {
90  title = tr("Select one or more files");
91  } else {
92  title = tr("Select file");
93  }
94  QString filter = tr("All files (*.*)");
95 
96  QStringList names;
97 
98  switch (type) {
99 
100  case SessionFile:
101  settingsKeyStub = "session";
102  if (multiple) {
103  title = tr("Select one or more session files");
104  } else {
105  title = tr("Select a session file");
106  }
107  filter = tr("%1 session files (*.%2)\nRDF files (%3)\nAll files (*.*)")
108  .arg(QApplication::applicationName())
109  .arg(m_sessionExtension)
110  .arg(RDFImporter::getKnownExtensions());
111  break;
112 
113  case AudioFile:
114  settingsKeyStub = "audio";
115  if (multiple) {
116  title = tr("Select one or more audio files");
117  } else {
118  title = tr("Select an audio file");
119  }
120  filter = tr("Audio files (%1)\nAll files (*.*)")
121  .arg(AudioFileReaderFactory::getKnownExtensions());
122  break;
123 
124  case LayerFile:
125  settingsKeyStub = "layer";
126  filter = tr("All supported files (%1 %2)\nSonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nRDF files (%2)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)")
127  .arg(DataFileReaderFactory::getKnownExtensions())
128  .arg(RDFImporter::getKnownExtensions());
129  break;
130 
131  case LayerFileNoMidi:
132  settingsKeyStub = "layer";
133  filter = tr("All supported files (%1 %2)\nSonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nRDF files (%2)\nText files (*.txt)\nAll files (*.*)")
134  .arg(DataFileReaderFactory::getKnownExtensions())
135  .arg(RDFImporter::getKnownExtensions());
136  break;
137 
138  case LayerFileNonSV:
139  settingsKeyStub = "layer";
140  filter = tr("All supported files (%1 %2)\nComma-separated data files (*.csv)\nSonic Visualiser Layer XML files (*.svl)\nSpace-separated .lab files (*.lab)\nRDF files (%2)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)")
141  .arg(DataFileReaderFactory::getKnownExtensions())
142  .arg(RDFImporter::getKnownExtensions());
143  break;
144 
145  case LayerFileNoMidiNonSV:
146  settingsKeyStub = "layer";
147  filter = tr("All supported files (%1 %2)\nComma-separated data files (*.csv)\nSonic Visualiser Layer XML files (*.svl)\nSpace-separated .lab files (*.lab)\nRDF files (%2)\nText files (*.txt)\nAll files (*.*)")
148  .arg(DataFileReaderFactory::getKnownExtensions())
149  .arg(RDFImporter::getKnownExtensions());
150  break;
151 
152  case SessionOrAudioFile:
153  settingsKeyStub = "last";
154  filter = tr("All supported files (*.%1 %2 %3)\n%4 session files (*.%1)\nAudio files (%3)\nRDF files (%2)\nAll files (*.*)")
155  .arg(m_sessionExtension)
156  .arg(RDFImporter::getKnownExtensions())
157  .arg(AudioFileReaderFactory::getKnownExtensions())
158  .arg(QApplication::applicationName());
159  break;
160 
161  case ImageFile:
162  settingsKeyStub = "image";
163  {
164  QStringList fmts;
165  QList<QByteArray> formats = QImageReader::supportedImageFormats();
166  for (QList<QByteArray>::iterator i = formats.begin();
167  i != formats.end(); ++i) {
168  fmts.push_back(QString("*.%1")
169  .arg(QString::fromLocal8Bit(*i).toLower()));
170  }
171  filter = tr("Image files (%1)\nAll files (*.*)").arg(fmts.join(" "));
172  }
173  break;
174 
175  case SVGFile:
176  settingsKeyStub = "svg";
177  filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
178  break;
179 
180  case CSVFile:
181  settingsKeyStub = "layer";
182  filter = tr("Comma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nText files (*.txt)\nAll files (*.*)");
183  break;
184 
185  case AnyFile:
186  settingsKeyStub = "last";
187  filter = tr("All supported files (*.%1 %2 %3 %4)\n%5 session files (*.%1)\nAudio files (%2)\nLayer files (%3)\nRDF files (%4)\nAll files (*.*)")
188  .arg(m_sessionExtension)
189  .arg(AudioFileReaderFactory::getKnownExtensions())
190  .arg(DataFileReaderFactory::getKnownExtensions())
191  .arg(RDFImporter::getKnownExtensions())
192  .arg(QApplication::applicationName());
193  break;
194  };
195 
196  if (lastPath == "") {
197  std::string home;
198  if (getEnvUtf8("HOME", home)) {
199  lastPath = QString::fromStdString(home);
200  } else {
201  lastPath = ".";
202  }
203  } else if (QFileInfo(lastPath).isDir()) {
204  lastPath = QFileInfo(lastPath).canonicalPath();
205  } else {
206  lastPath = QFileInfo(lastPath).absoluteDir().canonicalPath();
207  }
208 
209  QSettings settings;
210  settings.beginGroup("FileFinder");
211  lastPath = settings.value(settingsKeyStub + "path", lastPath).toString();
212 
213  // Use our own QFileDialog just for symmetry with getSaveFileName below
214 
215  QFileDialog dialog(m_parent);
216  dialog.setNameFilters(filter.split('\n'));
217  dialog.setWindowTitle(title);
218  dialog.setDirectory(lastPath);
219 
220  dialog.setAcceptMode(QFileDialog::AcceptOpen);
221 
222  if (multiple) {
223  dialog.setFileMode(QFileDialog::ExistingFiles);
224  } else {
225  dialog.setFileMode(QFileDialog::ExistingFile);
226  }
227 
228  QString testPath = "";
229  QString pathToRemember = "";
230 
231  if (dialog.exec()) {
232  names = dialog.selectedFiles();
233 
234  if (!multiple && !names.empty()) {
235  testPath = *names.begin();
236  QFileInfo fi(testPath);
237 
238  if (!fi.exists()) {
239  QMessageBox::critical(nullptr, tr("File does not exist"),
240  tr("<b>File not found</b><p>File \"%1\" does not exist").arg(testPath));
241 
242  } else if (!fi.isReadable()) {
243 
244  QMessageBox::critical(nullptr, tr("File is not readable"),
245  tr("<b>File is not readable</b><p>File \"%1\" can not be read").arg(testPath));
246 
247  } else if (fi.isDir()) {
248 
249  QMessageBox::critical(nullptr, tr("Directory selected"),
250  tr("<b>Directory selected</b><p>File \"%1\" is a directory").arg(testPath));
251 
252  } else if (!fi.isFile()) {
253 
254  QMessageBox::critical(nullptr, tr("Non-file selected"),
255  tr("<b>Not a file</b><p>Path \"%1\" is not a file").arg(testPath));
256 
257  } else if (fi.size() == 0) {
258 
259  QMessageBox::critical(nullptr, tr("File is empty"),
260  tr("<b>File is empty</b><p>File \"%1\" is empty").arg(testPath));
261 
262  } else {
263  pathToRemember = testPath;
264  }
265  }
266  }
267 
268  if (pathToRemember != "") {
269  settings.setValue(settingsKeyStub + "path",
270  QFileInfo(pathToRemember)
271  .absoluteDir()
272  .canonicalPath());
273  }
274 
275  return names;
276 }
277 
278 QString
280  QString fallbackLocation)
281 {
282  QString settingsKeyStub;
283  QString lastPath = fallbackLocation;
284 
285  QString title = tr("Select file");
286  QString filter = tr("All files (*.*)");
287 
288  switch (type) {
289 
290  case SessionFile:
291  settingsKeyStub = "savesession";
292  title = tr("Select a session file");
293  filter = tr("%1 session files (*.%2)\nAll files (*.*)")
294  .arg(QApplication::applicationName())
295  .arg(m_sessionExtension);
296  break;
297 
298  case AudioFile:
299  settingsKeyStub = "saveaudio";
300  title = "Select an audio file";
301  title = tr("Select a file to export to");
302  filter = tr("WAV audio files (*.wav)\nAll files (*.*)");
303  break;
304 
305  case LayerFile:
306  settingsKeyStub = "savelayer";
307  title = tr("Select a file to export to");
308  filter = tr("Sonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nRDF/Turtle files (%1)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)")
309  .arg(RDFExporter::getSupportedExtensions());
310  break;
311 
312  case LayerFileNoMidi:
313  settingsKeyStub = "savelayer";
314  title = tr("Select a file to export to");
315  filter = tr("Sonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nRDF/Turtle files (%1)\nText files (*.txt)\nAll files (*.*)")
316  .arg(RDFExporter::getSupportedExtensions());
317  break;
318 
319  case LayerFileNonSV:
320  settingsKeyStub = "savelayer";
321  title = tr("Select a file to export to");
322  filter = tr("Comma-separated data files (*.csv)\nSonic Visualiser Layer XML files (*.svl)\nRDF/Turtle files (%1)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)")
323  .arg(RDFExporter::getSupportedExtensions());
324  break;
325 
326  case LayerFileNoMidiNonSV:
327  settingsKeyStub = "savelayer";
328  title = tr("Select a file to export to");
329  filter = tr("Comma-separated data files (*.csv)\nSonic Visualiser Layer XML files (*.svl)\nRDF/Turtle files (%1)\nText files (*.txt)\nAll files (*.*)")
330  .arg(RDFExporter::getSupportedExtensions());
331  break;
332 
333  case SessionOrAudioFile:
334  cerr << "ERROR: Internal error: InteractiveFileFinder::getSaveFileName: SessionOrAudioFile cannot be used here" << endl;
335  abort();
336 
337  case ImageFile:
338  settingsKeyStub = "saveimage";
339  title = tr("Select a file to export to");
340  filter = tr("Portable Network Graphics files (*.png)\nAll files (*.*)");
341  break;
342 
343  case SVGFile:
344  settingsKeyStub = "savesvg";
345  title = tr("Select a file to export to");
346  filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
347  break;
348 
349  case CSVFile:
350  settingsKeyStub = "savelayer";
351  title = tr("Select a file to export to");
352  filter = tr("Comma-separated data files (*.csv)\nText files (*.txt)\nAll files (*.*)");
353  break;
354 
355  case AnyFile:
356  cerr << "ERROR: Internal error: InteractiveFileFinder::getSaveFileName: AnyFile cannot be used here" << endl;
357  abort();
358  };
359 
360  if (lastPath == "") {
361  std::string home;
362  if (getEnvUtf8("HOME", home)) {
363  lastPath = QString::fromStdString(home);
364  } else {
365  lastPath = ".";
366  }
367  } else if (QFileInfo(lastPath).isDir()) {
368  lastPath = QFileInfo(lastPath).canonicalPath();
369  } else {
370  lastPath = QFileInfo(lastPath).absoluteDir().canonicalPath();
371  }
372 
373  QSettings settings;
374  settings.beginGroup("FileFinder");
375  lastPath = settings.value(settingsKeyStub + "path", lastPath).toString();
376 
377  QString path = "";
378 
379  // Use our own QFileDialog instead of static functions, as we may
380  // need to adjust the file extension based on the selected filter
381 
382  QFileDialog dialog(m_parent);
383 
384  QStringList filters = filter.split('\n');
385 
386  dialog.setNameFilters(filters);
387  dialog.setWindowTitle(title);
388  dialog.setDirectory(lastPath);
389  dialog.setAcceptMode(QFileDialog::AcceptSave);
390  dialog.setFileMode(QFileDialog::AnyFile);
391  dialog.setOption(QFileDialog::DontConfirmOverwrite, true); // we'll do that
392 
393  QString defaultSuffix;
394  if (type == SessionFile) {
395  defaultSuffix = m_sessionExtension;
396  } else if (type == AudioFile) {
397  defaultSuffix = "wav";
398  } else if (type == ImageFile) {
399  defaultSuffix = "png";
400  } else if (type == SVGFile) {
401  defaultSuffix = "svg";
402  } else if (type == CSVFile) {
403  defaultSuffix = "csv";
404  }
405 
406  defaultSuffix =
407  settings.value(settingsKeyStub + "suffix", defaultSuffix).toString();
408 
409  dialog.setDefaultSuffix(defaultSuffix);
410 
411  foreach (QString f, filters) {
412  if (f.contains("." + defaultSuffix)) {
413  dialog.selectNameFilter(f);
414  }
415  }
416 
417  bool good = false;
418 
419  while (!good) {
420 
421  path = "";
422 
423  if (!dialog.exec()) break;
424 
425  QStringList files = dialog.selectedFiles();
426  if (files.empty()) break;
427  path = *files.begin();
428 
429  QFileInfo fi(path);
430 
431  cerr << "type = " << type << ", suffix = " << fi.suffix() << endl;
432 
433  if ((type == LayerFile || type == LayerFileNoMidi ||
434  type == LayerFileNonSV || type == LayerFileNoMidiNonSV)
435  && fi.suffix() == "") {
436  QString expectedExtension;
437  QString selectedFilter = dialog.selectedNameFilter();
438  if (selectedFilter.contains(".svl")) {
439  expectedExtension = "svl";
440  } else if (selectedFilter.contains(".txt")) {
441  expectedExtension = "txt";
442  } else if (selectedFilter.contains(".csv")) {
443  expectedExtension = "csv";
444  } else if (selectedFilter.contains(".mid")) {
445  expectedExtension = "mid";
446  } else if (selectedFilter.contains(".ttl")) {
447  expectedExtension = "ttl";
448  }
449  cerr << "expected extension = " << expectedExtension << endl;
450  if (expectedExtension != "") {
451  path = QString("%1.%2").arg(path).arg(expectedExtension);
452  fi = QFileInfo(path);
453  }
454  }
455 
456  if (fi.isDir()) {
457  QMessageBox::critical(nullptr, tr("Directory selected"),
458  tr("<b>Directory selected</b><p>File \"%1\" is a directory").arg(path));
459  continue;
460  }
461 
462  if (fi.exists()) {
463  if (QMessageBox::question(nullptr, tr("File exists"),
464  tr("<b>File exists</b><p>The file \"%1\" already exists.\nDo you want to overwrite it?").arg(path),
465  QMessageBox::Ok,
466  QMessageBox::Cancel) != QMessageBox::Ok) {
467  continue;
468  }
469  }
470 
471  good = true;
472  }
473 
474  if (path != "") {
475  settings.setValue(settingsKeyStub + "path",
476  QFileInfo(path).absoluteDir().canonicalPath());
477  settings.setValue(settingsKeyStub + "suffix",
478  QFileInfo(path).suffix());
479  }
480 
481  return path;
482 }
483 
484 void
486 {
487  QString settingsKeyStub;
488 
489  switch (type) {
490  case SessionFile:
491  settingsKeyStub = "session";
492  break;
493 
494  case AudioFile:
495  settingsKeyStub = "audio";
496  break;
497 
498  case LayerFile:
499  settingsKeyStub = "layer";
500  break;
501 
502  case LayerFileNoMidi:
503  settingsKeyStub = "layer";
504  break;
505 
506  case LayerFileNonSV:
507  settingsKeyStub = "layer";
508  break;
509 
510  case LayerFileNoMidiNonSV:
511  settingsKeyStub = "layer";
512  break;
513 
514  case SessionOrAudioFile:
515  settingsKeyStub = "last";
516  break;
517 
518  case ImageFile:
519  settingsKeyStub = "image";
520  break;
521 
522  case SVGFile:
523  settingsKeyStub = "svg";
524  break;
525 
526  case CSVFile:
527  settingsKeyStub = "layer";
528  break;
529 
530  case AnyFile:
531  settingsKeyStub = "last";
532  break;
533  }
534 
535  if (path != "") {
536  QSettings settings;
537  settings.beginGroup("FileFinder");
538  path = QFileInfo(path).absoluteDir().canonicalPath();
539  QString suffix = QFileInfo(path).suffix();
540  settings.setValue(settingsKeyStub + "path", path);
541  settings.setValue(settingsKeyStub + "suffix", suffix);
542  settings.setValue("lastpath", path);
543  }
544 }
545 
546 QString
547 InteractiveFileFinder::find(FileType type, QString location, QString lastKnownLocation)
548 {
549  if (FileSource::canHandleScheme(location)) {
550  if (FileSource(location).isAvailable()) {
551  SVDEBUG << "InteractiveFileFinder::find: ok, it's available... returning" << endl;
552  return location;
553  }
554  }
555 
556  if (QFileInfo(location).exists()) return location;
557 
558  QString foundAt = "";
559 
560  if ((foundAt = findRelative(location, lastKnownLocation)) != "") {
561  return foundAt;
562  }
563 
564  if ((foundAt = findRelative(location, m_lastLocatedLocation)) != "") {
565  return foundAt;
566  }
567 
568  return locateInteractive(type, location);
569 }
570 
571 QString
572 InteractiveFileFinder::findRelative(QString location, QString relativeTo)
573 {
574  if (relativeTo == "") return "";
575 
576  SVDEBUG << "Looking for \"" << location << "\" next to \""
577  << relativeTo << "\"..." << endl;
578 
579  QString fileName;
580  QString resolved;
581 
582  if (FileSource::isRemote(location)) {
583  fileName = QUrl(location).path().section('/', -1, -1,
584  QString::SectionSkipEmpty);
585  } else {
586  if (QUrl(location).scheme() == "file") {
587  location = QUrl(location).toLocalFile();
588  }
589  fileName = QFileInfo(location).fileName();
590  }
591 
592  if (FileSource::isRemote(relativeTo)) {
593  resolved = QUrl(relativeTo).resolved(fileName).toString();
594  if (!FileSource(resolved).isAvailable()) resolved = "";
595  cerr << "resolved: " << resolved << endl;
596  } else {
597  if (QUrl(relativeTo).scheme() == "file") {
598  relativeTo = QUrl(relativeTo).toLocalFile();
599  }
600  resolved = QFileInfo(relativeTo).dir().filePath(fileName);
601  if (!QFileInfo(resolved).exists() ||
602  !QFileInfo(resolved).isFile() ||
603  !QFileInfo(resolved).isReadable()) {
604  resolved = "";
605  }
606  }
607 
608  return resolved;
609 }
610 
611 QString
612 InteractiveFileFinder::locateInteractive(FileType type, QString thing)
613 {
614  QString question;
615  if (type == AudioFile) {
616  question = tr("<b>File not found</b><p>Audio file \"%1\" could not be opened.\nDo you want to locate it?");
617  } else {
618  question = tr("<b>File not found</b><p>File \"%1\" could not be opened.\nDo you want to locate it?");
619  }
620 
621  QString path = "";
622  bool done = false;
623 
624  while (!done) {
625 
626  int rv = QMessageBox::question
627  (nullptr,
628  tr("Failed to open file"),
629  question.arg(thing),
630  tr("Locate file..."),
631  tr("Use URL..."),
632  tr("Cancel"),
633  0, 2);
634 
635  switch (rv) {
636 
637  case 0: // Locate file
638 
639  if (QFileInfo(thing).dir().exists()) {
640  path = QFileInfo(thing).dir().canonicalPath();
641  }
642 
643  path = getOpenFileName(type, path);
644  done = (path != "");
645  break;
646 
647  case 1: // Use URL
648  {
649  bool ok = false;
650  path = QInputDialog::getText
651  (nullptr, tr("Use URL"),
652  tr("Please enter the URL to use for this file:"),
653  QLineEdit::Normal, "", &ok);
654 
655  if (ok && path != "") {
656  if (FileSource(path).isAvailable()) {
657  done = true;
658  } else {
659  QMessageBox::critical
660  (nullptr, tr("Failed to open location"),
661  tr("<b>Failed to open location</b><p>URL \"%1\" could not be opened").arg(path));
662  path = "";
663  }
664  }
665  break;
666  }
667 
668  case 2: // Cancel
669  path = "";
670  done = true;
671  break;
672  }
673  }
674 
675  if (path != "") m_lastLocatedLocation = path;
676  return path;
677 }
678 
679 
static void setParentWidget(QWidget *)
void registerLastOpenedFilePath(FileType type, QString path) override
QString locateInteractive(FileType type, QString thing)
QString getSaveFileName(FileType type, QString fallbackLocation="") override
void setApplicationSessionExtension(QString extension)
Specify the extension for this application&#39;s session files (without the dot)
QString getOpenFileName(FileType type, QString fallbackLocation="") override
QString find(FileType type, QString location, QString lastKnownLocation="") override
QStringList getOpenFileNames(FileType type, QString fallbackLocation="") override
QString findRelative(QString location, QString relativeTo)
static InteractiveFileFinder m_instance
static InteractiveFileFinder * getInstance()