Chris@378: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@378: 
Chris@378: /*
Chris@378:     Sonic Visualiser
Chris@378:     An audio file viewer and annotation editor.
Chris@378:     Centre for Digital Music, Queen Mary, University of London.
Chris@378:     This file copyright 2007 QMUL.
Chris@378:     
Chris@378:     This program is free software; you can redistribute it and/or
Chris@378:     modify it under the terms of the GNU General Public License as
Chris@378:     published by the Free Software Foundation; either version 2 of the
Chris@378:     License, or (at your option) any later version.  See the file
Chris@378:     COPYING included with this distribution for more information.
Chris@378: */
Chris@378: 
Chris@378: #include "FileFinder.h"
Chris@378: #include "data/fileio/FileSource.h"
Chris@378: #include "data/fileio/AudioFileReaderFactory.h"
Chris@378: #include "data/fileio/DataFileReaderFactory.h"
Chris@410: #include "rdf/RDFImporter.h"
Chris@456: #include "rdf/RDFExporter.h"
Chris@378: 
Chris@378: #include <QFileInfo>
Chris@378: #include <QMessageBox>
Chris@378: #include <QFileDialog>
Chris@378: #include <QInputDialog>
Chris@378: #include <QImageReader>
Chris@378: #include <QSettings>
Chris@378: 
Chris@378: #include <iostream>
Chris@378: 
Chris@378: FileFinder *
Chris@378: FileFinder::m_instance = 0;
Chris@378: 
Chris@378: FileFinder::FileFinder() :
Chris@378:     m_lastLocatedLocation("")
Chris@378: {
Chris@378: }
Chris@378: 
Chris@378: FileFinder::~FileFinder()
Chris@378: {
Chris@378: }
Chris@378: 
Chris@378: FileFinder *
Chris@378: FileFinder::getInstance()
Chris@378: {
Chris@378:     if (m_instance == 0) {
Chris@378:         m_instance = new FileFinder();
Chris@378:     }
Chris@378:     return m_instance;
Chris@378: }
Chris@378: 
Chris@378: QString
Chris@378: FileFinder::getOpenFileName(FileType type, QString fallbackLocation)
Chris@378: {
Chris@378:     QString settingsKey;
Chris@378:     QString lastPath = fallbackLocation;
Chris@378:     
Chris@378:     QString title = tr("Select file");
Chris@378:     QString filter = tr("All files (*.*)");
Chris@378: 
Chris@378:     switch (type) {
Chris@378: 
Chris@378:     case SessionFile:
Chris@378:         settingsKey = "sessionpath";
Chris@378:         title = tr("Select a session file");
Chris@456:         filter = tr("Sonic Visualiser session files (*.sv)\nRDF files (%1)\nAll files (*.*)").arg(RDFImporter::getKnownExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case AudioFile:
Chris@378:         settingsKey = "audiopath";
Chris@378:         title = "Select an audio file";
Chris@378:         filter = tr("Audio files (%1)\nAll files (*.*)")
Chris@378:             .arg(AudioFileReaderFactory::getKnownExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFile:
Chris@378:         settingsKey = "layerpath";
Chris@456:         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 (*.*)")
Chris@456:             .arg(DataFileReaderFactory::getKnownExtensions())
Chris@456:             .arg(RDFImporter::getKnownExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFileNoMidi:
Chris@378:         settingsKey = "layerpath";
Chris@456:         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 (*.*)")
Chris@456:             .arg(DataFileReaderFactory::getKnownExtensions())
Chris@456:             .arg(RDFImporter::getKnownExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case SessionOrAudioFile:
Chris@378:         settingsKey = "lastpath";
Chris@457:         filter = tr("All supported files (*.sv %1 %2)\nSonic Visualiser session files (*.sv)\nAudio files (%2)\nRDF files (%1)\nAll files (*.*)")
Chris@456:             .arg(RDFImporter::getKnownExtensions())
Chris@378:             .arg(AudioFileReaderFactory::getKnownExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case ImageFile:
Chris@378:         settingsKey = "imagepath";
Chris@378:         {
Chris@378:             QStringList fmts;
Chris@378:             QList<QByteArray> formats = QImageReader::supportedImageFormats();
Chris@378:             for (QList<QByteArray>::iterator i = formats.begin();
Chris@378:                  i != formats.end(); ++i) {
Chris@378:                 fmts.push_back(QString("*.%1")
Chris@378:                                .arg(QString::fromLocal8Bit(*i).toLower()));
Chris@378:             }
Chris@378:             filter = tr("Image files (%1)\nAll files (*.*)").arg(fmts.join(" "));
Chris@378:         }
Chris@378:         break;
Chris@378: 
Chris@378:     case AnyFile:
Chris@378:         settingsKey = "lastpath";
Chris@456:         filter = tr("All supported files (*.sv %1 %2 %3)\nSonic Visualiser session files (*.sv)\nAudio files (%1)\nLayer files (%2)\nRDF files (%3)\nAll files (*.*)")
Chris@378:             .arg(AudioFileReaderFactory::getKnownExtensions())
Chris@410:             .arg(DataFileReaderFactory::getKnownExtensions())
Chris@410:             .arg(RDFImporter::getKnownExtensions());
Chris@378:         break;
Chris@378:     };
Chris@378: 
Chris@378:     if (lastPath == "") {
Chris@378:         char *home = getenv("HOME");
Chris@378:         if (home) lastPath = home;
Chris@378:         else lastPath = ".";
Chris@378:     } else if (QFileInfo(lastPath).isDir()) {
Chris@378:         lastPath = QFileInfo(lastPath).canonicalPath();
Chris@378:     } else {
Chris@378:         lastPath = QFileInfo(lastPath).absoluteDir().canonicalPath();
Chris@378:     }
Chris@378: 
Chris@378:     QSettings settings;
Chris@378:     settings.beginGroup("FileFinder");
Chris@378:     lastPath = settings.value(settingsKey, lastPath).toString();
Chris@378: 
Chris@378:     QString path = "";
Chris@378: 
Chris@378:     // Use our own QFileDialog just for symmetry with getSaveFileName below
Chris@378: 
Chris@378:     QFileDialog dialog;
Chris@378:     dialog.setFilters(filter.split('\n'));
Chris@378:     dialog.setWindowTitle(title);
Chris@378:     dialog.setDirectory(lastPath);
Chris@378: 
Chris@378:     dialog.setAcceptMode(QFileDialog::AcceptOpen);
Chris@378:     dialog.setFileMode(QFileDialog::ExistingFile);
Chris@378:     
Chris@378:     if (dialog.exec()) {
Chris@378:         QStringList files = dialog.selectedFiles();
Chris@378:         if (!files.empty()) path = *files.begin();
Chris@378:         
Chris@378:         QFileInfo fi(path);
Chris@378:         
Chris@378:         if (!fi.exists()) {
Chris@378:             
Chris@378:             QMessageBox::critical(0, tr("File does not exist"),
Chris@460:                                   tr("<b>File not found</b><p>File \"%1\" does not exist").arg(path));
Chris@378:             path = "";
Chris@378:             
Chris@378:         } else if (!fi.isReadable()) {
Chris@378:             
Chris@378:             QMessageBox::critical(0, tr("File is not readable"),
Chris@460:                                   tr("<b>File is not readable</b><p>File \"%1\" can not be read").arg(path));
Chris@378:             path = "";
Chris@378:             
Chris@378:         } else if (fi.isDir()) {
Chris@378:             
Chris@378:             QMessageBox::critical(0, tr("Directory selected"),
Chris@460:                                   tr("<b>Directory selected</b><p>File \"%1\" is a directory").arg(path));
Chris@378:             path = "";
Chris@378: 
Chris@378:         } else if (!fi.isFile()) {
Chris@378:             
Chris@378:             QMessageBox::critical(0, tr("Non-file selected"),
Chris@460:                                   tr("<b>Not a file</b><p>Path \"%1\" is not a file").arg(path));
Chris@378:             path = "";
Chris@378:             
Chris@378:         } else if (fi.size() == 0) {
Chris@378:             
Chris@378:             QMessageBox::critical(0, tr("File is empty"),
Chris@460:                                   tr("<b>File is empty</b><p>File \"%1\" is empty").arg(path));
Chris@378:             path = "";
Chris@378:         }                
Chris@378:     }
Chris@378: 
Chris@378:     if (path != "") {
Chris@378:         settings.setValue(settingsKey,
Chris@378:                           QFileInfo(path).absoluteDir().canonicalPath());
Chris@378:     }
Chris@378:     
Chris@378:     return path;
Chris@378: }
Chris@378: 
Chris@378: QString
Chris@378: FileFinder::getSaveFileName(FileType type, QString fallbackLocation)
Chris@378: {
Chris@378:     QString settingsKey;
Chris@378:     QString lastPath = fallbackLocation;
Chris@378:     
Chris@378:     QString title = tr("Select file");
Chris@378:     QString filter = tr("All files (*.*)");
Chris@378: 
Chris@378:     switch (type) {
Chris@378: 
Chris@378:     case SessionFile:
Chris@378:         settingsKey = "savesessionpath";
Chris@378:         title = tr("Select a session file");
Chris@378:         filter = tr("Sonic Visualiser session files (*.sv)\nAll files (*.*)");
Chris@378:         break;
Chris@378: 
Chris@378:     case AudioFile:
Chris@378:         settingsKey = "saveaudiopath";
Chris@378:         title = "Select an audio file";
Chris@378:         title = tr("Select a file to export to");
Chris@378:         filter = tr("WAV audio files (*.wav)\nAll files (*.*)");
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFile:
Chris@378:         settingsKey = "savelayerpath";
Chris@378:         title = tr("Select a file to export to");
Chris@456:         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 (*.*)").arg(RDFExporter::getSupportedExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFileNoMidi:
Chris@378:         settingsKey = "savelayerpath";
Chris@378:         title = tr("Select a file to export to");
Chris@456:         filter = tr("Sonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nRDF/Turtle files (%1)\nText files (*.txt)\nAll files (*.*)").arg(RDFExporter::getSupportedExtensions());
Chris@378:         break;
Chris@378: 
Chris@378:     case SessionOrAudioFile:
Chris@378:         std::cerr << "ERROR: Internal error: FileFinder::getSaveFileName: SessionOrAudioFile cannot be used here" << std::endl;
Chris@378:         abort();
Chris@378: 
Chris@378:     case ImageFile:
Chris@378:         settingsKey = "saveimagepath";
Chris@378:         title = tr("Select a file to export to");
Chris@378:         filter = tr("Portable Network Graphics files (*.png)\nAll files (*.*)");
Chris@378:         break;
Chris@378: 
Chris@378:     case AnyFile:
Chris@378:         std::cerr << "ERROR: Internal error: FileFinder::getSaveFileName: AnyFile cannot be used here" << std::endl;
Chris@378:         abort();
Chris@378:     };
Chris@378: 
Chris@378:     if (lastPath == "") {
Chris@378:         char *home = getenv("HOME");
Chris@378:         if (home) lastPath = home;
Chris@378:         else lastPath = ".";
Chris@378:     } else if (QFileInfo(lastPath).isDir()) {
Chris@378:         lastPath = QFileInfo(lastPath).canonicalPath();
Chris@378:     } else {
Chris@378:         lastPath = QFileInfo(lastPath).absoluteDir().canonicalPath();
Chris@378:     }
Chris@378: 
Chris@378:     QSettings settings;
Chris@378:     settings.beginGroup("FileFinder");
Chris@378:     lastPath = settings.value(settingsKey, lastPath).toString();
Chris@378: 
Chris@378:     QString path = "";
Chris@378: 
Chris@378:     // Use our own QFileDialog instead of static functions, as we may
Chris@378:     // need to adjust the file extension based on the selected filter
Chris@378: 
Chris@378:     QFileDialog dialog;
Chris@378:     dialog.setFilters(filter.split('\n'));
Chris@378:     dialog.setWindowTitle(title);
Chris@378:     dialog.setDirectory(lastPath);
Chris@378: 
Chris@378:     dialog.setAcceptMode(QFileDialog::AcceptSave);
Chris@378:     dialog.setFileMode(QFileDialog::AnyFile);
Chris@378:     dialog.setConfirmOverwrite(false); // we'll do that
Chris@378:         
Chris@378:     if (type == SessionFile) {
Chris@378:         dialog.setDefaultSuffix("sv");
Chris@378:     } else if (type == AudioFile) {
Chris@378:         dialog.setDefaultSuffix("wav");
Chris@378:     } else if (type == ImageFile) {
Chris@378:         dialog.setDefaultSuffix("png");
Chris@378:     }
Chris@378: 
Chris@378:     bool good = false;
Chris@378: 
Chris@378:     while (!good) {
Chris@378: 
Chris@378:         path = "";
Chris@378:         
Chris@378:         if (!dialog.exec()) break;
Chris@378:         
Chris@378:         QStringList files = dialog.selectedFiles();
Chris@378:         if (files.empty()) break;
Chris@378:         path = *files.begin();
Chris@378:         
Chris@378:         QFileInfo fi(path);
Chris@378: 
Chris@378:         std::cerr << "type = " << type << ", suffix = " << fi.suffix().toStdString() << std::endl;
Chris@378:         
Chris@378:         if ((type == LayerFile || type == LayerFileNoMidi)
Chris@378:             && fi.suffix() == "") {
Chris@378:             QString expectedExtension;
Chris@378:             QString selectedFilter = dialog.selectedFilter();
Chris@378:             if (selectedFilter.contains(".svl")) {
Chris@378:                 expectedExtension = "svl";
Chris@378:             } else if (selectedFilter.contains(".txt")) {
Chris@378:                 expectedExtension = "txt";
Chris@378:             } else if (selectedFilter.contains(".csv")) {
Chris@378:                 expectedExtension = "csv";
Chris@378:             } else if (selectedFilter.contains(".mid")) {
Chris@378:                 expectedExtension = "mid";
Chris@456:             } else if (selectedFilter.contains(".ttl")) {
Chris@456:                 expectedExtension = "ttl";
Chris@378:             }
Chris@378:             std::cerr << "expected extension = " << expectedExtension.toStdString() << std::endl;
Chris@378:             if (expectedExtension != "") {
Chris@378:                 path = QString("%1.%2").arg(path).arg(expectedExtension);
Chris@378:                 fi = QFileInfo(path);
Chris@378:             }
Chris@378:         }
Chris@378:         
Chris@378:         if (fi.isDir()) {
Chris@378:             QMessageBox::critical(0, tr("Directory selected"),
Chris@460:                                   tr("<b>Directory selected</b><p>File \"%1\" is a directory").arg(path));
Chris@378:             continue;
Chris@378:         }
Chris@378:         
Chris@378:         if (fi.exists()) {
Chris@378:             if (QMessageBox::question(0, tr("File exists"),
Chris@460:                                       tr("<b>File exists</b><p>The file \"%1\" already exists.\nDo you want to overwrite it?").arg(path),
Chris@378:                                       QMessageBox::Ok,
Chris@378:                                       QMessageBox::Cancel) != QMessageBox::Ok) {
Chris@378:                 continue;
Chris@378:             }
Chris@378:         }
Chris@378:         
Chris@378:         good = true;
Chris@378:     }
Chris@378:         
Chris@378:     if (path != "") {
Chris@378:         settings.setValue(settingsKey,
Chris@378:                           QFileInfo(path).absoluteDir().canonicalPath());
Chris@378:     }
Chris@378:     
Chris@378:     return path;
Chris@378: }
Chris@378: 
Chris@378: void
Chris@378: FileFinder::registerLastOpenedFilePath(FileType type, QString path)
Chris@378: {
Chris@378:     QString settingsKey;
Chris@378: 
Chris@378:     switch (type) {
Chris@378:     case SessionFile:
Chris@378:         settingsKey = "sessionpath";
Chris@378:         break;
Chris@378: 
Chris@378:     case AudioFile:
Chris@378:         settingsKey = "audiopath";
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFile:
Chris@378:         settingsKey = "layerpath";
Chris@378:         break;
Chris@378: 
Chris@378:     case LayerFileNoMidi:
Chris@378:         settingsKey = "layerpath";
Chris@378:         break;
Chris@378: 
Chris@378:     case SessionOrAudioFile:
Chris@378:         settingsKey = "lastpath";
Chris@378:         break;
Chris@378: 
Chris@378:     case ImageFile:
Chris@378:         settingsKey = "imagepath";
Chris@378:         break;
Chris@378: 
Chris@378:     case AnyFile:
Chris@378:         settingsKey = "lastpath";
Chris@378:         break;
Chris@378:     }
Chris@378: 
Chris@378:     if (path != "") {
Chris@378:         QSettings settings;
Chris@378:         settings.beginGroup("FileFinder");
Chris@378:         path = QFileInfo(path).absoluteDir().canonicalPath();
Chris@378:         settings.setValue(settingsKey, path);
Chris@378:         settings.setValue("lastpath", path);
Chris@378:     }
Chris@378: }
Chris@378:     
Chris@378: QString
Chris@378: FileFinder::find(FileType type, QString location, QString lastKnownLocation)
Chris@378: {
Chris@378:     if (FileSource::canHandleScheme(location)) {
Chris@378:         if (FileSource(location).isAvailable()) {
Chris@378:             std::cerr << "FileFinder::find: ok, it's available... returning" << std::endl;
Chris@378:             return location;
Chris@378:         }
Chris@378:     }
Chris@378: 
Chris@378:     if (QFileInfo(location).exists()) return location;
Chris@378: 
Chris@378:     QString foundAt = "";
Chris@378: 
Chris@378:     if ((foundAt = findRelative(location, lastKnownLocation)) != "") {
Chris@378:         return foundAt;
Chris@378:     }
Chris@378: 
Chris@378:     if ((foundAt = findRelative(location, m_lastLocatedLocation)) != "") {
Chris@378:         return foundAt;
Chris@378:     }
Chris@378: 
Chris@378:     return locateInteractive(type, location);
Chris@378: }
Chris@378: 
Chris@378: QString
Chris@378: FileFinder::findRelative(QString location, QString relativeTo)
Chris@378: {
Chris@378:     if (relativeTo == "") return "";
Chris@378: 
Chris@378:     std::cerr << "Looking for \"" << location.toStdString() << "\" next to \""
Chris@378:               << relativeTo.toStdString() << "\"..." << std::endl;
Chris@378: 
Chris@378:     QString fileName;
Chris@378:     QString resolved;
Chris@378: 
Chris@378:     if (FileSource::isRemote(location)) {
Chris@378:         fileName = QUrl(location).path().section('/', -1, -1,
Chris@378:                                                  QString::SectionSkipEmpty);
Chris@378:     } else {
Chris@378:         if (QUrl(location).scheme() == "file") {
Chris@378:             location = QUrl(location).toLocalFile();
Chris@378:         }
Chris@378:         fileName = QFileInfo(location).fileName();
Chris@378:     }
Chris@378: 
Chris@378:     if (FileSource::isRemote(relativeTo)) {
Chris@378:         resolved = QUrl(relativeTo).resolved(fileName).toString();
Chris@378:         if (!FileSource(resolved).isAvailable()) resolved = "";
Chris@378:         std::cerr << "resolved: " << resolved.toStdString() << std::endl;
Chris@378:     } else {
Chris@378:         if (QUrl(relativeTo).scheme() == "file") {
Chris@378:             relativeTo = QUrl(relativeTo).toLocalFile();
Chris@378:         }
Chris@378:         resolved = QFileInfo(relativeTo).dir().filePath(fileName);
Chris@378:         if (!QFileInfo(resolved).exists() ||
Chris@378:             !QFileInfo(resolved).isFile() ||
Chris@378:             !QFileInfo(resolved).isReadable()) {
Chris@378:             resolved = "";
Chris@378:         }
Chris@378:     }
Chris@378:             
Chris@378:     return resolved;
Chris@378: }
Chris@378: 
Chris@378: QString
Chris@378: FileFinder::locateInteractive(FileType type, QString thing)
Chris@378: {
Chris@378:     QString question;
Chris@378:     if (type == AudioFile) {
Chris@460:         question = tr("<b>File not found</b><p>Audio file \"%1\" could not be opened.\nDo you want to locate it?");
Chris@378:     } else {
Chris@460:         question = tr("<b>File not found</b><p>File \"%1\" could not be opened.\nDo you want to locate it?");
Chris@378:     }
Chris@378: 
Chris@378:     QString path = "";
Chris@378:     bool done = false;
Chris@378: 
Chris@378:     while (!done) {
Chris@378: 
Chris@378:         int rv = QMessageBox::question
Chris@378:             (0, 
Chris@378:              tr("Failed to open file"),
Chris@378:              question.arg(thing),
Chris@378:              tr("Locate file..."),
Chris@378:              tr("Use URL..."),
Chris@378:              tr("Cancel"),
Chris@378:              0, 2);
Chris@378:         
Chris@378:         switch (rv) {
Chris@378: 
Chris@378:         case 0: // Locate file
Chris@378: 
Chris@378:             if (QFileInfo(thing).dir().exists()) {
Chris@378:                 path = QFileInfo(thing).dir().canonicalPath();
Chris@378:             }
Chris@378:             
Chris@378:             path = getOpenFileName(type, path);
Chris@378:             done = (path != "");
Chris@378:             break;
Chris@378: 
Chris@378:         case 1: // Use URL
Chris@378:         {
Chris@378:             bool ok = false;
Chris@378:             path = QInputDialog::getText
Chris@378:                 (0, tr("Use URL"),
Chris@378:                  tr("Please enter the URL to use for this file:"),
Chris@378:                  QLineEdit::Normal, "", &ok);
Chris@378: 
Chris@378:             if (ok && path != "") {
Chris@378:                 if (FileSource(path).isAvailable()) {
Chris@378:                     done = true;
Chris@378:                 } else {
Chris@378:                     QMessageBox::critical
Chris@378:                         (0, tr("Failed to open location"),
Chris@460:                          tr("<b>Failed to open location</b><p>URL \"%1\" could not be opened").arg(path));
Chris@378:                     path = "";
Chris@378:                 }
Chris@378:             }
Chris@378:             break;
Chris@378:         }
Chris@378: 
Chris@378:         case 2: // Cancel
Chris@378:             path = "";
Chris@378:             done = true;
Chris@378:             break;
Chris@378:         }
Chris@378:     }
Chris@378: 
Chris@378:     if (path != "") m_lastLocatedLocation = path;
Chris@378:     return path;
Chris@378: }
Chris@378: 
Chris@378: