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