diff --git a/fluid/fluid.cpp b/fluid/fluid.cpp index 6841cc059c..56a67ceb7e 100644 --- a/fluid/fluid.cpp +++ b/fluid/fluid.cpp @@ -21,6 +21,7 @@ #include "synthesizer/event.h" #include "synthesizer/msynthesizer.h" #include "mscore/preferences.h" +#include "mscore/extension.h" #include "fluid.h" #include "sfont.h" @@ -623,9 +624,7 @@ bool Fluid::loadSoundFonts(const QStringList& sl) locker.unlock(); bool ok = true; - QFileInfoList l = sfFiles(); - for (int i = sl.size() - 1; i >= 0; --i) { QString s = sl[i]; if (s.isEmpty()) @@ -896,6 +895,10 @@ QFileInfoList Fluid::sfFiles() QStringList pl = preferences.getString(PREF_APP_PATHS_MYSOUNDFONTS).split(";"); pl.prepend(QFileInfo(QString("%1%2").arg(mscoreGlobalShare).arg("sound")).absoluteFilePath()); + // append extensions directory + QStringList extensionsDir = Ms::Extension::getDirectoriesByType(Ms::Extension::soundfontsDir); + pl.append(extensionsDir); + foreach (const QString& s, pl) { QString ss(s); if (!s.isEmpty() && s[0] == '~') diff --git a/libmscore/instrtemplate.cpp b/libmscore/instrtemplate.cpp index 77b3280902..f883c2627d 100644 --- a/libmscore/instrtemplate.cpp +++ b/libmscore/instrtemplate.cpp @@ -52,6 +52,19 @@ static InstrumentGroup* searchInstrumentGroup(const QString& name) return nullptr; } +//--------------------------------------------------------- +// searchArticulation +//--------------------------------------------------------- + +static MidiArticulation searchArticulation(const QString& name) + { + foreach(MidiArticulation a, articulation) { + if (a.name == name) + return a; + } + return MidiArticulation(); + } + //--------------------------------------------------------- // readStaffIdx //--------------------------------------------------------- @@ -67,7 +80,7 @@ static int readStaffIdx(XmlReader& e) } //--------------------------------------------------------- -// readInstrumentGroup +// read InstrumentGroup //--------------------------------------------------------- void InstrumentGroup::read(XmlReader& e) @@ -108,6 +121,16 @@ void InstrumentGroup::read(XmlReader& e) id = name.toLower().replace(" ", "-"); } +//--------------------------------------------------------- +// clear InstrumentGroup +//--------------------------------------------------------- + +void InstrumentGroup::clear() + { + qDeleteAll(instrumentTemplates); + instrumentTemplates.clear(); + } + //--------------------------------------------------------- // InstrumentTemplate //--------------------------------------------------------- @@ -586,6 +609,21 @@ bool saveInstrumentTemplates1(const QString& instrTemplates) return true; } +//--------------------------------------------------------- +// clearInstrumentTemplates +//--------------------------------------------------------- + +void clearInstrumentTemplates() + { + for (InstrumentGroup* g : instrumentGroups) + g->clear(); + qDeleteAll(instrumentGroups); + instrumentGroups.clear(); + qDeleteAll(instrumentGenres); + instrumentGenres.clear(); + articulation.clear(); + } + //--------------------------------------------------------- // loadInstrumentTemplates //--------------------------------------------------------- @@ -604,8 +642,8 @@ bool loadInstrumentTemplates(const QString& instrTemplates) while (e.readNextStartElement()) { const QStringRef& tag(e.name()); if (tag == "instrument-group" || tag == "InstrumentGroup") { - QString id(e.attribute("id")); - InstrumentGroup* group = searchInstrumentGroup(id); + QString idGroup(e.attribute("id")); + InstrumentGroup* group = searchInstrumentGroup(idGroup); if (group == 0) { group = new InstrumentGroup; instrumentGroups.append(group); @@ -614,13 +652,14 @@ bool loadInstrumentTemplates(const QString& instrTemplates) } else if (tag == "Articulation") { // read global articulation - MidiArticulation a; + QString name(e.attribute("name")); + MidiArticulation a = searchArticulation(name); a.read(e); articulation.append(a); } else if (tag == "Genre") { - QString id(e.attribute("id")); - InstrumentGenre* genre = searchInstrumentGenre(id); + QString idGenre(e.attribute("id")); + InstrumentGenre* genre = searchInstrumentGenre(idGenre); if (!genre) { genre = new InstrumentGenre; instrumentGenres.append(genre); diff --git a/libmscore/instrtemplate.h b/libmscore/instrtemplate.h index 3f45da8c07..8b62bb2438 100644 --- a/libmscore/instrtemplate.h +++ b/libmscore/instrtemplate.h @@ -111,6 +111,7 @@ struct InstrumentGroup { bool extended; // belongs to extended instruments set if true QList instrumentTemplates; void read(XmlReader&); + void clear(); InstrumentGroup() { extended = false; } }; @@ -118,6 +119,7 @@ struct InstrumentGroup { extern QList instrumentGenres; extern QList articulation; extern QList instrumentGroups; +extern void clearInstrumentTemplates(); extern bool loadInstrumentTemplates(const QString& instrTemplates); extern bool saveInstrumentTemplates(const QString& instrTemplates); extern InstrumentTemplate* searchTemplate(const QString& name); diff --git a/libmscore/scorefile.cpp b/libmscore/scorefile.cpp index c804a8d38f..602cd7f9a7 100644 --- a/libmscore/scorefile.cpp +++ b/libmscore/scorefile.cpp @@ -781,7 +781,7 @@ Score::FileError MasterScore::loadCompressedMsc(QIODevice* io, bool ignoreVersio QByteArray dbuf = uz.fileData(rootfile); if (dbuf.isEmpty()) { - QList fil = uz.fileInfoList(); + QVector fil = uz.fileInfoList(); foreach(const MQZipReader::FileInfo& fi, fil) { if (fi.filePath.endsWith(".mscx")) { dbuf = uz.fileData(fi.filePath); diff --git a/libmscore/utils.cpp b/libmscore/utils.cpp index 8f3d00cf50..2caa941af0 100644 --- a/libmscore/utils.cpp +++ b/libmscore/utils.cpp @@ -669,6 +669,34 @@ int updateVersion() return _updateVersion; } +//--------------------------------------------------------- +// updateVersion +/// Up to 4 digits X.X.X.X +/// Each digit can be double XX.XX.XX.XX +/// return true if v1 < v2 +//--------------------------------------------------------- + +bool compareVersion(QString v1, QString v2) + { + auto v1l = v1.split("."); + auto v2l = v2.split("."); + int ma = qPow(100,qMax(v1l.size(), v2l.size())); + int m = ma; + int vv1 = 0; + for (int i = 0; i < v1l.size(); i++) { + vv1 += (m * v1l[i].toInt()); + m /= 100; + } + m = ma; + int vv2 = 0; + for (int i = 0; i < v2l.size(); i++) { + vv2 += (m * v2l[i].toInt()); + m /= 100; + } + + return vv1 < vv2; + } + //--------------------------------------------------------- // diatonicUpDown // used to find the second note of a trill, mordent etc. diff --git a/libmscore/utils.h b/libmscore/utils.h index ef3d0908f3..ed1ad20c16 100644 --- a/libmscore/utils.h +++ b/libmscore/utils.h @@ -65,6 +65,7 @@ extern int version(); extern int majorVersion(); extern int minorVersion(); extern int updateVersion(); +extern bool compareVersion(QString v1, QString v2); extern Note* nextChordNote(Note* note); extern Note* prevChordNote(Note* note); diff --git a/mscore/CMakeLists.txt b/mscore/CMakeLists.txt index 8aaf6f804c..1191580b73 100644 --- a/mscore/CMakeLists.txt +++ b/mscore/CMakeLists.txt @@ -351,6 +351,7 @@ add_executable ( ${ExecutableName} abstractdialog.cpp abstractdialog.h toolbuttonmenu.cpp preferenceslistwidget.cpp preferenceslistwidget.h + extension.cpp extension.h ${COCOABRIDGE} ${OMR_FILES} diff --git a/mscore/downloadUtils.cpp b/mscore/downloadUtils.cpp index de26977c7d..61ab894d5c 100644 --- a/mscore/downloadUtils.cpp +++ b/mscore/downloadUtils.cpp @@ -36,7 +36,6 @@ bool DownloadUtils::saveFile() void DownloadUtils::downloadFinished(QNetworkReply *data) { sdata = data->readAll(); - qDebug() << "size" << sdata.size(); emit done(); } @@ -45,16 +44,31 @@ QByteArray DownloadUtils::returnData() return sdata; } -void DownloadUtils::download() +void DownloadUtils::download(bool showProgress) { QUrl url = QUrl::fromEncoded(_target.toLocal8Bit()); QNetworkRequest request(url); QEventLoop loop; QNetworkReply* reply = manager.get(request); + QObject::connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgress(qint64,qint64))); QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinished(QNetworkReply*))); QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + + if (showProgress) { + progressDialog = new QProgressDialog(static_cast(parent())); + progressDialog->setWindowFlags(Qt::WindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowTitleHint)); + progressDialog->setWindowModality(Qt::ApplicationModal); + progressDialog->setCancelButtonText(tr("Cancel")); + progressDialog->setLabelText(tr("Downloading...")); + progressDialog->setAutoClose(true); + progressDialog->setAutoReset(true); + QObject::connect(progressDialog, SIGNAL(canceled()), &loop, SLOT(quit())); + progressDialog->show(); + } + loop.exec(); + QObject::disconnect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgress(qint64,qint64))); QObject::disconnect(&manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinished(QNetworkReply*))); QObject::disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); @@ -62,7 +76,9 @@ void DownloadUtils::download() void DownloadUtils::downloadProgress(qint64 received, qint64 total) { - qDebug() << (double(received)/total)*100 << "%"; + double curVal = (double(received)/total)*100; + if (progressDialog && progressDialog->isVisible()) + progressDialog->setValue(curVal); } } diff --git a/mscore/downloadUtils.h b/mscore/downloadUtils.h index a1972659f7..d9c92c51b0 100644 --- a/mscore/downloadUtils.h +++ b/mscore/downloadUtils.h @@ -24,6 +24,8 @@ class DownloadUtils : public QObject QString _target; QString _localFile; + QProgressDialog* progressDialog = nullptr; + public: explicit DownloadUtils(QWidget *parent=0); @@ -36,7 +38,7 @@ class DownloadUtils : public QObject void done(); public slots: - void download(); + void download(bool showProgress = false); void downloadFinished(QNetworkReply* data); void downloadProgress(qint64 received, qint64 total); }; diff --git a/mscore/extension.cpp b/mscore/extension.cpp new file mode 100644 index 0000000000..bd0bb6b02c --- /dev/null +++ b/mscore/extension.cpp @@ -0,0 +1,71 @@ +//============================================================================= +// MusE Score +// Linux Music Score Editor +// $Id:$ +// +// Copyright (C) 2018 Werner Schweer and others +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +//============================================================================= + +#include "extension.h" +#include "preferences.h" +#include "libmscore/utils.h" + +namespace Ms { + +//--------------------------------------------------------- +// getDirectoriesByType +//--------------------------------------------------------- + +QStringList Extension::getDirectoriesByType(const char* type) + { + QStringList result; + QDir d(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)); + for (auto dd : d.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot| QDir::Readable | QDir::NoSymLinks)) { + QDir extensionsDir(dd.absoluteFilePath()); + auto extDir = extensionsDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot| QDir::Readable | QDir::NoSymLinks, QDir::Name); + // take the most recent version only + if (!extDir.isEmpty()) { + QString typeDir = QString("%1/%2").arg(extDir.last().absoluteFilePath()).arg(type); + if (QFileInfo(typeDir).exists()) + result.append(typeDir); + } + } + return result; + } + +//--------------------------------------------------------- +// isInstalled +//--------------------------------------------------------- + +bool Extension::isInstalled(QString extensionId) + { + QDir extensionDir(QString("%1/%2").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId)); + return extensionDir.exists(); + } + +//--------------------------------------------------------- +// getLatestVersion +//--------------------------------------------------------- + +QString Extension::getLatestVersion(QString extensionId) + { + QString result = "0.0"; + QDir extensionDir(QString("%1/%2").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId)); + auto extDir = extensionDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot| QDir::Readable | QDir::NoSymLinks, QDir::Name); + if (!extDir.isEmpty()) + result = extDir.last().fileName(); + return result; + } +} diff --git a/mscore/extension.h b/mscore/extension.h new file mode 100644 index 0000000000..0361b5d350 --- /dev/null +++ b/mscore/extension.h @@ -0,0 +1,47 @@ +//============================================================================= +// MusE Score +// Linux Music Score Editor +// $Id:$ +// +// Copyright (C) 2018 Werner Schweer and others +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +//============================================================================= + +#ifndef __EXTENSION_H__ +#define __EXTENSION_H__ + +namespace Ms { + +//--------------------------------------------------------- +// Extension +//--------------------------------------------------------- + +class Extension { + public: + Extension() {} + static constexpr const char* workspacesDir = "workspaces"; + static constexpr const char* sfzsDir = "sfzs"; + static constexpr const char* soundfontsDir = "soundfonts"; + static constexpr const char* templatesDir = "templates"; + static constexpr const char* instrumentsDir = "instruments"; + + static QStringList getDirectoriesByType(const char* type); + static bool isInstalled(QString extensionId); + static QString getLatestVersion(QString extensionId); + }; + + +} // namespace Ms +#endif + diff --git a/mscore/file.cpp b/mscore/file.cpp index 3a50eda23d..ba7c946c6e 100644 --- a/mscore/file.cpp +++ b/mscore/file.cpp @@ -74,6 +74,7 @@ #include "synthesizer/msynthesizer.h" #include "svggenerator.h" #include "scorePreview.h" +#include "extension.h" #ifdef OMR #include "omr/omr.h" @@ -90,6 +91,7 @@ namespace Ms { extern void importSoundfont(QString name); + extern bool savePositions(Score*, const QString& name, bool segments); extern MasterSynthesizer* synti; @@ -2120,6 +2122,15 @@ void importSoundfont(QString name) } } +//--------------------------------------------------------- +// importExtension +//--------------------------------------------------------- + +void importExtension(QString name) + { + mscore->importExtension(name); + } + //--------------------------------------------------------- // readScore /// Import file \a name @@ -2143,6 +2154,10 @@ Score::FileError readScore(MasterScore* score, QString name, bool ignoreVersionE importSoundfont(name); return Score::FileError::FILE_IGNORE_ERROR; } + else if (suffix == "muxt") { + importExtension(name); + return Score::FileError::FILE_IGNORE_ERROR; + } else { // typedef Score::FileError (*ImportFunction)(MasterScore*, const QString&); struct ImportDef { diff --git a/mscore/instrdialog.cpp b/mscore/instrdialog.cpp index 78e3bb04f0..dbe99fbc03 100644 --- a/mscore/instrdialog.cpp +++ b/mscore/instrdialog.cpp @@ -52,7 +52,8 @@ InstrumentsDialog::InstrumentsDialog(QWidget* parent) QAction* a = getAction("instruments"); connect(a, SIGNAL(triggered()), SLOT(reject())); addAction(a); - + saveButton->setVisible(false); + loadButton->setVisible(false); readSettings(); } @@ -172,6 +173,24 @@ QTreeWidget* InstrumentsDialog::partiturList() } //--------------------------------------------------------- +// buildInstrumentsList +//--------------------------------------------------------- + +void InstrumentsDialog::buildInstrumentsList() + { + instrumentsWidget->buildTemplateList(); + } + +//--------------------------------------------------------- +// updateInstrumentDialog +//--------------------------------------------------------- + +void MuseScore::updateInstrumentDialog() + { + if (instrList) + instrList->buildInstrumentsList(); + } +//--------------------------------------------------------- // editInstrList //--------------------------------------------------------- diff --git a/mscore/instrdialog.h b/mscore/instrdialog.h index 44efc20cdd..43fb6f6092 100644 --- a/mscore/instrdialog.h +++ b/mscore/instrdialog.h @@ -38,6 +38,7 @@ class InstrumentsDialog : public QDialog, public Ui::InstrumentsDialog { void writeSettings(); void genPartList(Score*); QTreeWidget* partiturList(); + void buildInstrumentsList(); }; } // namespace Ms diff --git a/mscore/instrwidget.cpp b/mscore/instrwidget.cpp index b593ee2755..b9e08e4b41 100644 --- a/mscore/instrwidget.cpp +++ b/mscore/instrwidget.cpp @@ -413,7 +413,7 @@ void populateGenreCombo(QComboBox* combo) void populateInstrumentList(QTreeWidget* instrumentList) { instrumentList->clear(); - // TODO: memory leak + // TODO: memory leak? foreach(InstrumentGroup* g, instrumentGroups) { InstrumentTemplateListItem* group = new InstrumentTemplateListItem(g->name, instrumentList); group->setFlags(Qt::ItemIsEnabled); diff --git a/mscore/musescore.cpp b/mscore/musescore.cpp index 196b66243e..b31a67204d 100644 --- a/mscore/musescore.cpp +++ b/mscore/musescore.cpp @@ -94,6 +94,7 @@ #include "libmscore/lasso.h" #include "libmscore/excerpt.h" #include "libmscore/synthesizerstate.h" +#include "libmscore/utils.h" #include "driver.h" @@ -111,6 +112,8 @@ #include "startcenter.h" #include "help.h" #include "awl/aslider.h" +#include "extension.h" +#include "thirdparty/qzip/qzipreader_p.h" #ifdef USE_LAME #include "exportmp3.h" @@ -163,6 +166,7 @@ static QString jsonFileName; static QString audioDriver; static QString pluginName; static QString styleFile; +static QString extensionName; static bool scoresOnCommandline { false }; static QList translatorList; @@ -465,6 +469,7 @@ void updateExternalValuesFromPreferences() { dir.mkpath(preferences.getString(PREF_APP_PATHS_MYSTYLES)); dir.mkpath(preferences.getString(PREF_APP_PATHS_MYIMAGES)); dir.mkpath(preferences.getString(PREF_APP_PATHS_MYTEMPLATES)); + dir.mkpath(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)); dir.mkpath(preferences.getString(PREF_APP_PATHS_MYPLUGINS)); foreach (QString path, preferences.getString(PREF_APP_PATHS_MYSOUNDFONTS).split(";")) dir.mkpath(path); @@ -551,6 +556,8 @@ void MuseScore::preferencesChanged() delete newWizard; newWizard = 0; + reloadInstrumentTemplates(); + updateInstrumentDialog(); } //--------------------------------------------------------- @@ -609,6 +616,291 @@ void MuseScore::populateNoteInputMenu() } } +//--------------------------------------------------------- +// onLongOperationFinished +//--------------------------------------------------------- + +void MuseScore::onLongOperationFinished() + { + infoMsgBox->accept(); + } + +//--------------------------------------------------------- +// importExtension +//--------------------------------------------------------- + +bool MuseScore::importExtension(QString path) + { + MQZipReader zipFile(path); + // compute total unzipped size + qint64 totalZipSize = 0; + for (auto fi : zipFile.fileInfoList()) + totalZipSize += fi.size; + + // check if extension path is writable and has enough space + QStorageInfo storage = QStorageInfo(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)); + if (storage.isReadOnly()) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Cannot import extension on read-only storage: %1").arg(storage.displayName())); + return false; + } + if (totalZipSize >= storage.bytesAvailable()) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Cannot import extension: storage %1 is full").arg(storage.displayName())); + return false; + } + // Check structure of the extension + bool hasMetadata = false; + bool hasAlienDirectory = false; + bool hasAlienFiles = false; + QSet acceptableFolders = { Extension::sfzsDir, Extension::soundfontsDir, Extension::templatesDir, Extension::instrumentsDir, Extension::workspacesDir }; + for (auto fi : zipFile.fileInfoList()) { + if (fi.filePath == "metadata.json") + hasMetadata = true; + else { + // get folders + auto path = QDir::cleanPath(fi.filePath); + QStringList folders(path); + while ((path = QFileInfo(path).path()).length() < folders.last().length()) + folders << path; + if (folders.size() < 2) { + hasAlienFiles = true; // in root dir + break; + } + QString rootDir = folders.at(folders.size() - 2); + if (!acceptableFolders.contains(rootDir)) { + hasAlienDirectory = true; // in root dir + break; + } + } + } + if (!hasMetadata) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Corrupted extension: no metadata.json")); + return false; + } + if (hasAlienDirectory) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Corrupted extension: unsupported directories in root directory")); + return false; + } + if (hasAlienFiles) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Corrupted extension: unsupported files in root directory")); + return false; + } + zipFile.close(); + + MQZipReader zipFile2(path); + // get extension id from metadata.json + QByteArray mdba = zipFile2.fileData("metadata.json"); + zipFile2.close(); + QJsonDocument loadDoc = QJsonDocument::fromJson(mdba); + QJsonObject mdObject = loadDoc.object(); + QString extensionId = mdObject["id"].toString(); + QString version = mdObject["version"].toString(); + if (extensionId.isEmpty() || version.isEmpty()) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Corrupted extension: corrupted metadata.json")); + return false; + } + + // Check if extension is already installed, ask for uninstall + QDir dir(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)); + auto dirList = dir.entryList(QStringList(extensionId), QDir::Dirs | QDir::NoDotAndDotDot); + bool newerVersion = false; + if (dirList.contains(extensionId)) { + QString extDirName = QString("%1/%2").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId); + QDir extDir(extDirName); + auto versionDirList = extDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + if (versionDirList.size() > 0) { + // potentially other versions + // is there a more recent version? + for (auto versionDir : versionDirList) { + if (compareVersion(version, versionDir)) { + qDebug() << "There is a newer version. We don't install"; + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("A newer version is already installed")); + newerVersion = true; + return false; + } + } + } + if (!newerVersion) { + qDebug() << "found already install extension without newer version: deleting it"; + QDir d(QString("%1/%2").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId)); + if (!d.removeRecursively()) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Error while deleting previous version of the extension: %1").arg(extensionId)); + return false; + } + } + } + + //setup the message box + infoMsgBox = new QMessageBox(); + infoMsgBox->setWindowModality(Qt::ApplicationModal); + infoMsgBox->setWindowFlags(Qt::WindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowTitleHint)); + infoMsgBox->setTextFormat(Qt::RichText); + infoMsgBox->setMinimumSize(300, 100); + infoMsgBox->setMaximumSize(300, 100); + infoMsgBox->setStandardButtons(0); + infoMsgBox->setText(QString("

") + tr("Please wait, unpacking extension...") + QString("

")); + + //setup async run of long operations + QFutureWatcher futureWatcherUnzip; + connect(&futureWatcherUnzip, SIGNAL(finished()), this, SLOT(onLongOperationFinished())); + + MQZipReader* zipFile3 = new MQZipReader(path); + // Unzip the extension asynchronously + QFuture futureUnzip = QtConcurrent::run(zipFile3, &MQZipReader::extractAll, QString("%1/%2/%3").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version)); + futureWatcherUnzip.setFuture(futureUnzip); + if (!MScore::noGui) + infoMsgBox->exec(); + bool unzipResult = futureUnzip.result(); + zipFile3->close(); + delete zipFile3; + zipFile3 = nullptr; + + if (!unzipResult) { + if (!MScore::noGui) + QMessageBox::critical(mscore, QWidget::tr("Import Extension File"), QWidget::tr("Unable to extract files from the extension")); + return false; + } + + delete newWizard; + newWizard = 0; + mscore->reloadInstrumentTemplates(); + mscore->updateInstrumentDialog(); + + auto loadSoundFontAsync = [&]() { + // After install: add sfz to zerberus + QDir sfzDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::sfzsDir)); + if (sfzDir.exists()) { + // get all sfz files + QDirIterator it(sfzDir.absolutePath(), QStringList("*.sfz"), QDir::Files, QDirIterator::Subdirectories); + Synthesizer* s = synti->synthesizer("Zerberus"); + QStringList sfzs; + while (it.hasNext()) { + it.next(); + sfzs.append(it.fileName()); + } + sfzs.sort(); + for (int sfzNum = 0; sfzNum < sfzs.size(); ++sfzNum) + s->addSoundFont(sfzs[sfzNum]); + + if (!sfzs.isEmpty()) + synti->storeState(); + } + + // After install: add soundfont to fluid + QDir sfDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::soundfontsDir)); + if (sfDir.exists()) { + // get all soundfont files + QStringList filters("*.sf2"); + filters.append("*.sf3"); + QDirIterator it(sfzDir.absolutePath(), filters, QDir::Files, QDirIterator::Subdirectories); + Synthesizer* s = synti->synthesizer("Fluid"); + QStringList sfs; + while (it.hasNext()) { + it.next(); + sfs.append(it.fileName()); + } + sfs.sort(); + for (auto sf : sfs) + s->addSoundFont(sf); + if (!sfs.isEmpty()) + synti->storeState(); + } + }; + if (!enableExperimental) { + //load soundfonts async + QFuture futureLoadSFs = QtConcurrent::run(loadSoundFontAsync); + QFutureWatcher futureWatcherLoadSFs; + futureWatcherLoadSFs.setFuture(futureLoadSFs); + connect(&futureWatcherLoadSFs, SIGNAL(finished()), this, SLOT(onLongOperationFinished())); + infoMsgBox->setText(QString("

") + tr("Please wait, loading soundfonts...") + QString("

")); + if (!MScore::noGui) + infoMsgBox->exec(); + else + futureLoadSFs.waitForFinished(); + } + + // after install: refresh workspaces if needed + QDir workspacesDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::workspacesDir)); + if (workspacesDir.exists() && !MScore::noGui) { + auto wsList = workspacesDir.entryInfoList(QStringList("*.workspace"), QDir::Files); + if (!wsList.isEmpty()) { + Workspace::refreshWorkspaces(); + paletteBox->updateWorkspaces(); + paletteBox->selectWorkspace(wsList.last().absoluteFilePath()); + } + } + return true; + } + +//--------------------------------------------------------- +// uninstallExtension +//--------------------------------------------------------- + +bool MuseScore::uninstallExtension(QString extensionId) + { + QString version = Extension::getLatestVersion(extensionId); + // Before install: remove sfz from zerberus + QDir sfzDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::sfzsDir)); + if (sfzDir.exists()) { + // get all sfz files + QDirIterator it(sfzDir.absolutePath(), QStringList("*.sfz"), QDir::Files, QDirIterator::Subdirectories); + Synthesizer* s = synti->synthesizer("Zerberus"); + bool found = it.hasNext(); + while (it.hasNext()) { + it.next(); + s->removeSoundFont(it.fileInfo().absoluteFilePath()); + } + if (found) + synti->storeState(); + } + // Before install: remove soundfont from fluid + QDir sfDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::soundfontsDir)); + if (sfDir.exists()) { + // get all soundfont files + QStringList filters("*.sf2"); + filters.append("*.sf3"); + QDirIterator it(sfzDir.absolutePath(), filters, QDir::Files, QDirIterator::Subdirectories); + Synthesizer* s = synti->synthesizer("Fluid"); + bool found = it.hasNext(); + while (it.hasNext()) { + it.next(); + s->removeSoundFont(it.fileInfo().absoluteFilePath()); + } + if (found) + synti->storeState(); + } + bool refreshWorkspaces = false; + QDir workspacesDir(QString("%1/%2/%3/%4").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId).arg(version).arg(Extension::workspacesDir)); + if (workspacesDir.exists()) { + auto wsList = workspacesDir.entryInfoList(QStringList("*.workspace"), QDir::Files); + if (!wsList.isEmpty()) + refreshWorkspaces = true; + } + + // delete directories + QDir extensionDir(QString("%1/%2").arg(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)).arg(extensionId)); + extensionDir.removeRecursively(); + + // update UI + delete newWizard; + newWizard = 0; + mscore->reloadInstrumentTemplates(); + mscore->updateInstrumentDialog(); + if (refreshWorkspaces) { + Workspace::refreshWorkspaces(); + paletteBox->updateWorkspaces(); + paletteBox->selectWorkspace("Basic"); + } + return true; + } + //--------------------------------------------------------- // MuseScore //--------------------------------------------------------- @@ -646,7 +938,8 @@ MuseScore::MuseScore() setWindowTitle(QString(MUSESCORE_NAME_VERSION)); setIconSize(QSize(preferences.getInt(PREF_UI_THEME_ICONWIDTH) * guiScaling, preferences.getInt(PREF_UI_THEME_ICONHEIGHT) * guiScaling)); - ucheck = new UpdateChecker(); + ucheck = new UpdateChecker(this); + packUChecker = new ExtensionsUpdateChecker(this); setAcceptDrops(true); setFocusPolicy(Qt::NoFocus); @@ -1395,14 +1688,9 @@ MuseScore::MuseScore() setCentralWidget(envelope); - // load cascading instrument templates - loadInstrumentTemplates(preferences.getString(PREF_APP_PATHS_INSTRUMENTLIST1)); - QString instrList2 = preferences.getString(PREF_APP_PATHS_INSTRUMENTLIST2); - if (!instrList2.isEmpty()) - loadInstrumentTemplates(instrList2); - if (!MScore::noGui) preferencesChanged(); + if (seq) { connect(seq, SIGNAL(started()), SLOT(seqStarted())); connect(seq, SIGNAL(stopped()), SLOT(seqStopped())); @@ -1818,6 +2106,31 @@ void MuseScore::openRecentMenu() } } +//--------------------------------------------------------- +// reloadInstrumentTemplates +//--------------------------------------------------------- + +void MuseScore::reloadInstrumentTemplates() + { + clearInstrumentTemplates(); + // load cascading instrument templates + loadInstrumentTemplates(preferences.getString(PREF_APP_PATHS_INSTRUMENTLIST1)); + QString list2 = preferences.getString(PREF_APP_PATHS_INSTRUMENTLIST2); + if (!list2.isEmpty()) + loadInstrumentTemplates(list2); + + // load instrument templates from extension + QStringList extensionDir = Extension::getDirectoriesByType(Extension::instrumentsDir); + QStringList filter("*.xml"); + for (QString s : extensionDir) { + QDir extDir(s); + extDir.setNameFilters(filter); + auto instFiles = extDir.entryInfoList(QDir::Files | QDir::NoSymLinks | QDir::Readable); + for (auto instFile : instFiles) + loadInstrumentTemplates(instFile.absoluteFilePath()); + } + } + //--------------------------------------------------------- // setCurrentView //--------------------------------------------------------- @@ -2898,14 +3211,23 @@ static bool processNonGui(const QStringList& argv) if (!converterMode) return res; } - bool rv = true; if (converterMode) { if (processJob) return doProcessJob(jsonFileName); else return convert(argv[0], outFileName); } - return rv; + if (!extensionName.isEmpty()) { + QFileInfo fi(extensionName); + QString suffix = fi.suffix().toLower(); + if (suffix == "muxt") + return mscore->importExtension(extensionName); + else { + fprintf(stderr, "cannot install extension: <%s>\n", qPrintable(extensionName)); + return false; + } + } + return true; } //--------------------------------------------------------- @@ -3069,7 +3391,16 @@ bool MuseScore::hasToCheckForUpdate() if (ucheck) return ucheck->hasToCheck(); else - return false; + return false; + } + +//--------------------------------------------------------- +// hasToCheckForExtensionsUpdate +//--------------------------------------------------------- + +bool MuseScore::hasToCheckForExtensionsUpdate() + { + return packUChecker ? packUChecker->hasToCheck() : false; } //--------------------------------------------------------- @@ -3082,6 +3413,16 @@ void MuseScore::checkForUpdate() ucheck->check(version(), sender() != 0); } +//--------------------------------------------------------- +// checkForExtensionsUpdate +//--------------------------------------------------------- + +void MuseScore::checkForExtensionsUpdate() + { + if (packUChecker) + packUChecker->check(); + } + //--------------------------------------------------------- // readLanguages //--------------------------------------------------------- @@ -5501,6 +5842,8 @@ void MuseScore::showSearchDialog() _searchDialog->show(); } + + #ifndef SCRIPT_INTERFACE void MuseScore::pluginTriggered(int) {} void MuseScore::loadPlugins() {} @@ -6037,6 +6380,7 @@ int main(int argc, char* av[]) parser.addOption(QCommandLineOption( "no-fallback-font", "Don't use Bravura as fallback musical font")); parser.addOption(QCommandLineOption({"f", "force"}, "Used with '-o ', ignore warnings reg. score being corrupted or from wrong version")); parser.addOption(QCommandLineOption({"b", "bitrate"}, "Used with '-o .mp3', sets bitrate, in kbps", "bitrate")); + parser.addOption(QCommandLineOption({"E", "install-extension"}, "Install an extension, load soundfont as default unless if -e is passed too", "extension file")); parser.addPositionalArgument("scorefiles", "The files to open", "[scorefile...]"); @@ -6083,6 +6427,10 @@ int main(int argc, char* av[]) if (pluginName.isEmpty()) parser.showHelp(EXIT_FAILURE); } + if (parser.isSet("E")) { + MScore::noGui = true; + extensionName = parser.value("E"); + } MScore::saveTemplateMode = parser.isSet("template-mode"); if (parser.isSet("r")) { QString temp = parser.value("r"); @@ -6278,7 +6626,6 @@ int main(int argc, char* av[]) MScore::readDefaultStyle(preferences.getString(PREF_SCORE_STYLE_DEFAULTSTYLEFILE)); QSplashScreen* sc = 0; - QTimer* stimer = 0; if (!MScore::noGui && preferences.getBool(PREF_UI_APP_STARTUP_SHOWSPLASHSCREEN)) { QPixmap pm(":/data/splash.png"); sc = new QSplashScreen(pm); @@ -6286,10 +6633,6 @@ int main(int argc, char* av[]) #ifdef Q_OS_MAC // to have session dialog on top of splashscreen on mac sc->setWindowFlags(Qt::FramelessWindowHint); #endif - // show splash screen for 5 sec - stimer = new QTimer(0); - qApp->connect(stimer, SIGNAL(timeout()), sc, SLOT(close())); - stimer->start(5000); sc->show(); qApp->processEvents(); } @@ -6459,6 +6802,9 @@ int main(int argc, char* av[]) mscore->checkForUpdate(); #endif + if (mscore->hasToCheckForExtensionsUpdate()) + mscore->checkForExtensionsUpdate(); + if (!scoresOnCommandline && preferences.getBool(PREF_UI_APP_STARTUP_SHOWSTARTCENTER) && (!restoredSession || mscore->scores().size() == 0)) { #ifdef Q_OS_MAC // ugly, but on mac we get an event when a file is open. @@ -6481,6 +6827,7 @@ int main(int argc, char* av[]) #endif } + sc->close(); mscore->showPlayPanel(preferences.getBool(PREF_UI_APP_STARTUP_SHOWPLAYPANEL)); QSettings settings; if (settings.value("synthControlVisible", false).toBool()) diff --git a/mscore/musescore.h b/mscore/musescore.h index 09346d2800..b6653e5c37 100644 --- a/mscore/musescore.h +++ b/mscore/musescore.h @@ -222,7 +222,8 @@ class MuseScore : public QMainWindow, public MuseScoreCore { QSettings settings; ScoreView* cv { 0 }; ScoreState _sstate; - UpdateChecker* ucheck = nullptr; + UpdateChecker* ucheck; + ExtensionsUpdateChecker* packUChecker = nullptr; static const std::list _allNoteInputMenuEntries; static const std::list _basicNoteInputMenuEntries; @@ -409,6 +410,7 @@ class MuseScore : public QMainWindow, public MuseScoreCore { qreal _physicalDotsPerInch; + QMessageBox* infoMsgBox; //--------------------- virtual void closeEvent(QCloseEvent*); @@ -511,6 +513,7 @@ class MuseScore : public QMainWindow, public MuseScoreCore { void switchLayoutMode(int); void showMidiImportPanel(); void changeWorkspace(QAction*); + void onLongOperationFinished(); virtual QMenu* createPopupMenu() override; @@ -526,6 +529,7 @@ class MuseScore : public QMainWindow, public MuseScoreCore { void setPlayState() { changeState(STATE_PLAY); } void setNoteEntryState() { changeState(STATE_NOTE_ENTRY); } void checkForUpdate(); + void checkForExtensionsUpdate(); void midiNoteReceived(int channel, int pitch, int velo); void midiNoteReceived(int pitch, bool ctrl, int velo); void instrumentChanged(); @@ -608,6 +612,7 @@ class MuseScore : public QMainWindow, public MuseScoreCore { QNetworkAccessManager* networkManager(); virtual Score* openScore(const QString& fn); bool hasToCheckForUpdate(); + bool hasToCheckForExtensionsUpdate(); static bool unstable(); bool eventFilter(QObject *, QEvent *); void setMidiRecordId(int id) { _midiRecordId = id; } @@ -752,6 +757,8 @@ class MuseScore : public QMainWindow, public MuseScoreCore { QHelpEngine* helpEngine() const { return _helpEngine; } virtual void updateInspector() override; + void updateInstrumentDialog(); + void reloadInstrumentTemplates(); void showSynthControl(bool); void showMixer(bool); @@ -777,6 +784,8 @@ class MuseScore : public QMainWindow, public MuseScoreCore { static void restoreGeometry(QWidget*const qw); void updateWindowTitle(Score* score); + bool importExtension(QString path); + bool uninstallExtension(QString extensionId); }; extern MuseScore* mscore; diff --git a/mscore/newwizard.cpp b/mscore/newwizard.cpp index 6063af3be5..c036e5b293 100644 --- a/mscore/newwizard.cpp +++ b/mscore/newwizard.cpp @@ -1,3 +1,4 @@ + //============================================================================= // MusE Score // Linux Music Score Editor @@ -24,6 +25,7 @@ #include "palette.h" #include "instrdialog.h" #include "scoreBrowser.h" +#include "extension.h" #include "libmscore/instrtemplate.h" #include "libmscore/score.h" @@ -274,18 +276,9 @@ NewWizardPage4::NewWizardPage4(QWidget* parent) templateFileBrowser = new ScoreBrowser; templateFileBrowser->setStripNumbers(true); - QDir dir(mscoreGlobalShare + "/templates"); - QFileInfoList fil = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Dirs | QDir::Files, QDir::Name); - if(fil.isEmpty()){ - fil.append(QFileInfo(QFile(":data/Empty_Score.mscz"))); - } - - QDir myTemplatesDir(preferences.getString(PREF_APP_PATHS_MYTEMPLATES)); - fil.append(myTemplatesDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Dirs | QDir::Files, QDir::Name)); - templateFileBrowser->setShowCustomCategory(true); - templateFileBrowser->setScores(fil); templateFileBrowser->setSizePolicy(QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored)); + buildTemplatesList(); QVBoxLayout* layout = new QVBoxLayout; QHBoxLayout* searchLayout = new QHBoxLayout; @@ -317,6 +310,31 @@ void NewWizardPage4::initializePage() path.clear(); } +//--------------------------------------------------------- +// buildTemplatesList +//--------------------------------------------------------- + +void NewWizardPage4::buildTemplatesList() + { + + QDir dir(mscoreGlobalShare + "/templates"); + QFileInfoList fil = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Dirs | QDir::Files, QDir::Name); + if(fil.isEmpty()){ + fil.append(QFileInfo(QFile(":data/Empty_Score.mscz"))); + } + + QDir myTemplatesDir(preferences.getString(PREF_APP_PATHS_MYTEMPLATES)); + fil.append(myTemplatesDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Dirs | QDir::Files, QDir::Name)); + + // append templates directories from extensions + QStringList extensionsDir = Extension::getDirectoriesByType(Extension::templatesDir); + for (QString extDir : extensionsDir) { + QDir extTemplateDir(extDir); + fil.append(extTemplateDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Dirs | QDir::Files, QDir::Name)); + } + templateFileBrowser->setScores(fil); + } + //--------------------------------------------------------- // isComplete //--------------------------------------------------------- diff --git a/mscore/newwizard.h b/mscore/newwizard.h index d2ce4bebac..6be37a9ac6 100644 --- a/mscore/newwizard.h +++ b/mscore/newwizard.h @@ -145,6 +145,7 @@ class NewWizardPage4 : public QWizardPage { virtual bool isComplete() const override; QString templatePath() const; virtual void initializePage(); + void buildTemplatesList(); }; //--------------------------------------------------------- @@ -207,6 +208,7 @@ class NewWizard : public QWizard { double tempo() const { return p5->tempo(); } bool createTempo() const { return p5->createTempo(); } bool emptyScore() const; + void updateValues() const; }; diff --git a/mscore/palettebox.cpp b/mscore/palettebox.cpp index e2980c059d..2b730c3850 100644 --- a/mscore/palettebox.cpp +++ b/mscore/palettebox.cpp @@ -170,6 +170,16 @@ void PaletteBox::updateWorkspaces() workspaceList->setCurrentIndex(curIdx); } +//--------------------------------------------------------- +// selectWorkspace +//--------------------------------------------------------- + +void PaletteBox::selectWorkspace(QString path) + { + int idx = workspaceList->findData(path); + workspaceList->setCurrentIndex(idx); + } + //--------------------------------------------------------- // clear //--------------------------------------------------------- diff --git a/mscore/palettebox.h b/mscore/palettebox.h index c4ca0f5a92..1b4585a46f 100644 --- a/mscore/palettebox.h +++ b/mscore/palettebox.h @@ -68,6 +68,7 @@ class PaletteBox : public QDockWidget { bool eventFilter(QObject* obj, QEvent *event); void setKeyboardNavigation(bool val) { keyboardNavigation = val; } bool getKeyboardNavigation() { return keyboardNavigation; } + void selectWorkspace(QString path); }; //--------------------------------------------------------- diff --git a/mscore/preferences.cpp b/mscore/preferences.cpp index 0970ec65cd..34f7581c40 100644 --- a/mscore/preferences.cpp +++ b/mscore/preferences.cpp @@ -32,7 +32,6 @@ void Preferences::init(bool storeInMemoryOnly) if (!storeInMemoryOnly) { if (_settings) delete _settings; - _settings = new QSettings(); } @@ -40,8 +39,10 @@ void Preferences::init(bool storeInMemoryOnly) #if defined(Q_OS_MAC) || (defined(Q_OS_WIN) && !defined(FOR_WINSTORE)) bool checkUpdateStartup = true; + bool checkExtensionsUpdateStartup = true; #else bool checkUpdateStartup = false; + bool checkExtensionsUpdateStartup = false; #endif #if defined(Q_OS_MAC) || defined(Q_OS_WIN) @@ -51,7 +52,6 @@ void Preferences::init(bool storeInMemoryOnly) #else bool nativeDialogs = false; // don't use system native file dialogs #endif - bool defaultUsePortAudio = false; bool defaultUsePulseAudio = false; bool defaultUseJackAudio = false; @@ -84,6 +84,7 @@ void Preferences::init(bool storeInMemoryOnly) {PREF_APP_PATHS_MYSHORTCUTS, new StringPreference(QFileInfo(QString("%1/%2").arg(wd).arg(QCoreApplication::translate("shortcuts_directory", "Shortcuts"))).absoluteFilePath(), false)}, {PREF_APP_PATHS_MYSTYLES, new StringPreference(QFileInfo(QString("%1/%2").arg(wd).arg(QCoreApplication::translate("styles_directory", "Styles"))).absoluteFilePath(), false)}, {PREF_APP_PATHS_MYTEMPLATES, new StringPreference(QFileInfo(QString("%1/%2").arg(wd).arg(QCoreApplication::translate("templates_directory", "Templates"))).absoluteFilePath(), false)}, + {PREF_APP_PATHS_MYEXTENSIONS, new StringPreference(QFileInfo(QString("%1/%2").arg(wd).arg(QCoreApplication::translate("extensions_directory", "Extensions"))).absoluteFilePath(), false)}, {PREF_APP_PLAYBACK_FOLLOWSONG, new BoolPreference(true)}, {PREF_APP_PLAYBACK_PANPLAYBACK, new BoolPreference(true)}, {PREF_APP_PLAYBACK_PLAYREPEATS, new BoolPreference(true)}, @@ -150,6 +151,7 @@ void Preferences::init(bool storeInMemoryOnly) {PREF_UI_CANVAS_SCROLL_LIMITSCROLLAREA, new BoolPreference(false, false)}, {PREF_UI_CANVAS_SCROLL_VERTICALORIENTATION, new BoolPreference(false, false)}, {PREF_UI_APP_STARTUP_CHECKUPDATE, new BoolPreference(checkUpdateStartup, false)}, + {PREF_UI_APP_STARTUP_CHECK_EXTENSIONS_UPDATE, new BoolPreference(checkExtensionsUpdateStartup, false)}, {PREF_UI_APP_STARTUP_SHOWNAVIGATOR, new BoolPreference(false, false)}, {PREF_UI_APP_STARTUP_SHOWPLAYPANEL, new BoolPreference(false, false)}, {PREF_UI_APP_STARTUP_SHOWSPLASHSCREEN, new BoolPreference(true, false)}, @@ -404,7 +406,6 @@ void Preferences::clearMidiRemote(int recordId) remove(baseKey); } - Preference::Preference(QVariant defaultValue, QMetaType::Type type, bool showInAdvancedList) : _defaultValue(defaultValue), _showInAdvancedList(showInAdvancedList), diff --git a/mscore/preferences.h b/mscore/preferences.h index 0432cbe129..75393e8b9c 100644 --- a/mscore/preferences.h +++ b/mscore/preferences.h @@ -91,6 +91,7 @@ enum class MusicxmlExportBreaks : char { #define PREF_APP_PATHS_MYSOUNDFONTS "application/paths/mySoundfonts" #define PREF_APP_PATHS_MYSTYLES "application/paths/myStyles" #define PREF_APP_PATHS_MYTEMPLATES "application/paths/myTemplates" +#define PREF_APP_PATHS_MYEXTENSIONS "application/paths/myExtensions" #define PREF_APP_PLAYBACK_FOLLOWSONG "application/playback/followSong" #define PREF_APP_PLAYBACK_PANPLAYBACK "application/playback/panPlayback" #define PREF_APP_PLAYBACK_PLAYREPEATS "application/playback/playRepeats" @@ -158,6 +159,7 @@ enum class MusicxmlExportBreaks : char { #define PREF_UI_CANVAS_SCROLL_VERTICALORIENTATION "ui/canvas/scroll/verticalOrientation" #define PREF_UI_CANVAS_SCROLL_LIMITSCROLLAREA "ui/canvas/scroll/limitScrollArea" #define PREF_UI_APP_STARTUP_CHECKUPDATE "ui/application/startup/checkUpdate" +#define PREF_UI_APP_STARTUP_CHECK_EXTENSIONS_UPDATE "ui/application/startup/checkExtensionsUpdate" #define PREF_UI_APP_STARTUP_SHOWNAVIGATOR "ui/application/startup/showNavigator" #define PREF_UI_APP_STARTUP_SHOWPLAYPANEL "ui/application/startup/showPlayPanel" #define PREF_UI_APP_STARTUP_SHOWSPLASHSCREEN "ui/application/startup/showSplashScreen" diff --git a/mscore/prefsdialog.cpp b/mscore/prefsdialog.cpp index bca4b7bfdb..7262801c5e 100644 --- a/mscore/prefsdialog.cpp +++ b/mscore/prefsdialog.cpp @@ -153,6 +153,8 @@ PreferenceDialog::PreferenceDialog(QWidget* parent) connect(myPluginsButton, SIGNAL(clicked()), SLOT(selectPluginsDirectory())); connect(myImagesButton, SIGNAL(clicked()), SLOT(selectImagesDirectory())); connect(mySoundfontsButton, SIGNAL(clicked()), SLOT(changeSoundfontPaths())); + connect(myExtensionsButton, SIGNAL(clicked()), SLOT(selectExtensionsDirectory())); + connect(updateTranslation, SIGNAL(clicked()), SLOT(updateTranslationClicked())); @@ -532,6 +534,7 @@ void PreferenceDialog::updateValues(bool useDefaultValues) myTemplates->setText(preferences.getString(PREF_APP_PATHS_MYTEMPLATES)); myPlugins->setText(preferences.getString(PREF_APP_PATHS_MYPLUGINS)); mySoundfonts->setText(preferences.getString(PREF_APP_PATHS_MYSOUNDFONTS)); + myExtensions->setText(preferences.getString(PREF_APP_PATHS_MYEXTENSIONS)); index = exportAudioSampleRate->findData(preferences.getInt(PREF_EXPORT_AUDIO_SAMPLERATE)); exportAudioSampleRate->setCurrentIndex(index); @@ -898,6 +901,7 @@ void PreferenceDialog::apply() preferences.setPreference(PREF_APP_PATHS_MYSOUNDFONTS, mySoundfonts->text()); preferences.setPreference(PREF_APP_PATHS_MYSTYLES, myStyles->text()); preferences.setPreference(PREF_APP_PATHS_MYTEMPLATES, myTemplates->text()); + preferences.setPreference(PREF_APP_PATHS_MYEXTENSIONS, myExtensions->text()); preferences.setPreference(PREF_APP_STARTUP_STARTSCORE, sessionScore->text()); preferences.setPreference(PREF_EXPORT_AUDIO_SAMPLERATE, exportAudioSampleRate->currentData().toInt()); preferences.setPreference(PREF_EXPORT_MP3_BITRATE, exportMp3BitRate->currentData().toInt()); @@ -1300,6 +1304,22 @@ void PreferenceDialog::changeSoundfontPaths() mySoundfonts->setText(pld.path()); } +//--------------------------------------------------------- +// selectExtensionsDirectory +//--------------------------------------------------------- + +void PreferenceDialog::selectExtensionsDirectory() + { + QString s = QFileDialog::getExistingDirectory( + this, + tr("Choose Extensions Folder"), + myExtensions->text(), + QFileDialog::ShowDirsOnly | (preferences.getBool(PREF_UI_APP_USENATIVEDIALOGS) ? QFileDialog::Options() : QFileDialog::DontUseNativeDialog) + ); + if (!s.isNull()) + myExtensions->setText(s); + } + //--------------------------------------------------------- // updateLanguagesClicked //--------------------------------------------------------- @@ -1307,6 +1327,7 @@ void PreferenceDialog::changeSoundfontPaths() void PreferenceDialog::updateTranslationClicked() { ResourceManager r(0); + r.selectLanguagesTab(); r.exec(); } diff --git a/mscore/prefsdialog.h b/mscore/prefsdialog.h index 3278b7c25f..10f75b4597 100644 --- a/mscore/prefsdialog.h +++ b/mscore/prefsdialog.h @@ -76,6 +76,7 @@ class PreferenceDialog : public AbstractDialog, private Ui::PrefsDialogBase { void selectTemplatesDirectory(); void selectPluginsDirectory(); void selectImagesDirectory(); + void selectExtensionsDirectory(); void printShortcutsClicked(); void filterShortcutsTextChanged(const QString &); void filterAdvancedPreferences(const QString&); diff --git a/mscore/prefsdialog.ui b/mscore/prefsdialog.ui index 90e9f34e74..f8b8931d8e 100644 --- a/mscore/prefsdialog.ui +++ b/mscore/prefsdialog.ui @@ -428,6 +428,19 @@ + + + + Extensions: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 5 + + + @@ -588,6 +601,16 @@ + + + + Extensions folder + + + Insert path to extensions folder + + + @@ -628,6 +651,26 @@ + + + + Qt::StrongFocus + + + Extensions folder + + + Opens a folder dialog for selecting the extensions folder + + + + + + + :/data/icons/document-open.svg:/data/icons/document-open.svg + + + @@ -1047,6 +1090,7 @@ + 0 20 @@ -4109,7 +4153,20 @@ Adjusting latency can help synchronize your MIDI hardware with MuseScore's inter Automatic Update Check - + + + + Qt::Vertical + + + + 20 + 40 + + + + + @@ -4126,19 +4183,6 @@ Adjusting latency can help synchronize your MIDI hardware with MuseScore's inter - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -4146,6 +4190,13 @@ Adjusting latency can help synchronize your MIDI hardware with MuseScore's inter + + + + Check for new version of MuseScore extensions + + + @@ -4261,6 +4312,8 @@ Adjusting latency can help synchronize your MIDI hardware with MuseScore's inter mySoundfontsButton myImages myImagesButton + myExtensions + myExtensionsButton language updateTranslation styleName diff --git a/mscore/resourceManager.cpp b/mscore/resourceManager.cpp index a21db9ac33..e6be8bc56b 100644 --- a/mscore/resourceManager.cpp +++ b/mscore/resourceManager.cpp @@ -12,6 +12,9 @@ #include "resourceManager.h" #include "musescore.h" +#include "extension.h" +#include "libmscore/utils.h" +#include "stringutils.h" #include "ui_resourceManager.h" #include "thirdparty/qzip/qzipreader_p.h" @@ -29,28 +32,130 @@ ResourceManager::ResourceManager(QWidget *parent) : setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); QDir dir; dir.mkpath(dataPath + "/locale"); - baseAddr = "http://extensions.musescore.org/2.3/"; - displayPlugins(); + displayExtensions(); displayLanguages(); languagesTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); languagesTable->verticalHeader()->hide(); - tabs->removeTab(tabs->indexOf(plugins)); - tabs->setCurrentIndex(tabs->indexOf(languages)); + extensionsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + extensionsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); + extensionsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed); + extensionsTable->verticalHeader()->hide(); + extensionsTable->setColumnWidth(1, 50); + extensionsTable->setColumnWidth(1, 100); MuseScore::restoreGeometry(this); } -void ResourceManager::displayPlugins() + +//--------------------------------------------------------- +// selectLanguagesTab +//--------------------------------------------------------- + +void ResourceManager::selectLanguagesTab() { - textBrowser->setText("hello"); + tabs->setCurrentIndex(tabs->indexOf(languages)); } +//--------------------------------------------------------- +// selectExtensionsTab +//--------------------------------------------------------- + +void ResourceManager::selectExtensionsTab() + { + tabs->setCurrentIndex(tabs->indexOf(extensions)); + } + + +//--------------------------------------------------------- +// displayExtensions +//--------------------------------------------------------- + +void ResourceManager::displayExtensions() + { + DownloadUtils js(this); + js.setTarget(baseAddr() + "extensions/details.json"); + js.download(); + QByteArray json = js.returnData(); + + // parse the json file + QJsonParseError err; + QJsonDocument result = QJsonDocument::fromJson(json, &err); + if (err.error != QJsonParseError::NoError || !result.isObject()) { + qDebug("An error occurred during parsing"); + return; + } + int rowCount = result.object().keys().size(); + rowCount -= 2; //version and type + extensionsTable->setRowCount(rowCount); + + int row = 0; + int col = 0; + QPushButton* buttonInstall; + QPushButton* buttonUninstall; + extensionsTable->verticalHeader()->show(); + + QStringList extensions = result.object().keys(); + for (QString key : extensions) { + if (!result.object().value(key).isObject()) + continue; + QJsonObject value = result.object().value(key).toObject(); + col = 0; + QString test = value.value("file_name").toString(); + if (test.length() == 0) + continue; + + QString filename = value.value("file_name").toString(); + QString name = value.value("name").toString(); + int fileSize = value.value("file_size").toInt(); + QString hashValue = value.value("hash").toString(); + QString version = value.value("version").toString(); + + extensionsTable->setItem(row, col++, new QTableWidgetItem(name)); + extensionsTable->setItem(row, col++, new QTableWidgetItem(version)); + extensionsTable->setItem(row, col++, new QTableWidgetItem(stringutils::convertFileSizeToHumanReadable(fileSize))); + buttonInstall = new QPushButton(tr("Install")); + buttonUninstall = new QPushButton(tr("Uninstall")); + + connect(buttonInstall, SIGNAL(clicked()), this, SLOT(downloadExtension())); + connect(buttonUninstall, SIGNAL(clicked()), this, SLOT(uninstallExtension())); + buttonInstall->setProperty("path", "extensions/" + filename); + buttonInstall->setProperty("hash", hashValue); + buttonInstall->setProperty("rowId", row); + buttonUninstall->setProperty("extensionId", key); + buttonUninstall->setProperty("rowId", row); + + // get the installed version of the extension if any + if (Extension::isInstalled(key)) { + buttonUninstall->setDisabled(false); + QString installedVersion = Extension::getLatestVersion(key); + if (compareVersion(installedVersion, version)) { + buttonInstall->setText(tr("Update")); + } + else { + buttonInstall->setText(tr("Updated")); + buttonInstall->setDisabled(true); + } + } + else { + buttonUninstall->setDisabled(true); + } + extensionsTable->setIndexWidget(extensionsTable->model()->index(row, col++), buttonInstall); + extensionsTable->setIndexWidget(extensionsTable->model()->index(row, col++), buttonUninstall); + row++; + } + } + +//--------------------------------------------------------- +// displayLanguages +//--------------------------------------------------------- + void ResourceManager::displayLanguages() { // Download details.json - DownloadUtils *js = new DownloadUtils(this); - js->setTarget(baseAddr + "languages/details.json"); - js->download(); - QByteArray json = js->returnData(); + DownloadUtils js(this); + js.download(); + js.setTarget(baseAddr() + "languages/details.json"); + QByteArray json = js.returnData(); + qDebug() << json; // parse the json file QJsonParseError err; @@ -70,7 +175,7 @@ void ResourceManager::displayLanguages() languagesTable->verticalHeader()->show(); // move current language to first row - QStringList languages = result.object().keys(); + QStringList languages = result.object().keys(); QString lang = mscore->getLocaleISOCode(); int index = languages.indexOf(lang); if (index < 0 && lang.size() > 2) { @@ -98,12 +203,12 @@ void ResourceManager::displayLanguages() languagesTable->setItem(row, col++, new QTableWidgetItem(name)); languagesTable->setItem(row, col++, new QTableWidgetItem(filename)); - languagesTable->setItem(row, col++, new QTableWidgetItem(tr("%1 KB").arg(fileSize))); + languagesTable->setItem(row, col++, new QTableWidgetItem(tr("%1 kB").arg(fileSize))); updateButtons[row] = new QPushButton(tr("Update")); temp = updateButtons[row]; - buttonMap[temp] = "languages/" + filename; - buttonHashMap[temp] = hashValue; + languageButtonMap[temp] = "languages/" + filename; + languageButtonHashMap[temp] = hashValue; languagesTable->setIndexWidget(languagesTable->model()->index(row, col++), temp); @@ -121,16 +226,20 @@ void ResourceManager::displayLanguages() bool verifyInstruments = verifyLanguageFile(filenameInstruments, hashInstruments); if (verifyMScore && verifyInstruments) { // compare local file with distant hash - temp->setText(tr("No update")); + temp->setText(tr("Updated")); temp->setDisabled(1); } else { - connect(temp, SIGNAL(clicked()), this, SLOT(download())); + connect(temp, SIGNAL(clicked()), this, SLOT(downloadLanguage())); } row++; } } +//--------------------------------------------------------- +// verifyLanguageFile +//--------------------------------------------------------- + bool ResourceManager::verifyLanguageFile(QString filename, QString hash) { QString local = dataPath + "/locale/" + filename; @@ -143,21 +252,25 @@ bool ResourceManager::verifyLanguageFile(QString filename, QString hash) return verifyFile(local, hash); } -void ResourceManager::download() + +//--------------------------------------------------------- +// downloadLanguage +//--------------------------------------------------------- + +void ResourceManager::downloadLanguage() { - QPushButton *button = qobject_cast( sender() ); - QString data = buttonMap[button]; - QString hash = buttonHashMap[button]; + QPushButton *button = static_cast( sender() ); + QString data = languageButtonMap[button]; + QString hash = languageButtonHashMap[button]; button->setText(tr("Updating")); - button->setDisabled(1); - QString baseAddress = baseAddr + data; - DownloadUtils *dl = new DownloadUtils(this); - dl->setTarget(baseAddress); - qDebug() << baseAddress; + button->setDisabled(true); + QString baseAddress = baseAddr() + data; + DownloadUtils dl(this); + dl.setTarget(baseAddress); QString localPath = dataPath + "/locale/" + data.split('/')[1]; - dl->setLocalFile(localPath); - dl->download(); - if( !dl->saveFile() || !verifyFile(localPath, hash)) { + dl.setLocalFile(localPath); + dl.download(); + if (!dl.saveFile() || !verifyFile(localPath, hash)) { button->setText(tr("Failed, try again")); button->setEnabled(1); } @@ -166,7 +279,7 @@ void ResourceManager::download() MQZipReader zipFile(localPath); QFileInfo zfi(localPath); QString destinationDir(zfi.absolutePath()); - QList allFiles = zipFile.fileInfoList(); + QVector allFiles = zipFile.fileInfoList(); bool result = true; foreach (MQZipReader::FileInfo fi, allFiles) { const QString absPath = destinationDir + "/" + fi.filePath; @@ -186,7 +299,7 @@ void ResourceManager::download() QFile::remove(localPath); button->setText(tr("Updated")); // retranslate the UI if current language is updated - if (data == buttonMap.first()) + if (data == languageButtonMap.first()) setMscoreLocale(localeName); } else { @@ -196,6 +309,79 @@ void ResourceManager::download() } } +//--------------------------------------------------------- +// downloadExtension +//--------------------------------------------------------- + +void ResourceManager::downloadExtension() + { + QPushButton* button = static_cast(sender()); + QString data = button->property("path").toString(); + QString hash = button->property("hash").toString(); + button->setText(tr("Updating")); + button->setDisabled(true); + QString baseAddress = baseAddr() + data; + DownloadUtils dl(this); + dl.setTarget(baseAddress); + QString localPath = QDir::tempPath() + QDir::separator() + data.split('/')[1]; + QFile::remove(localPath); + dl.setLocalFile(localPath); + dl.download(true); + bool saveFileRes = dl.saveFile(); + bool verifyFileRes = saveFileRes && verifyFile(localPath, hash); + if(!verifyFileRes) { + QFile::remove(localPath); + button->setText(tr("Failed, try again")); + button->setEnabled(true); + + QMessageBox msgBox; + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setTextFormat(Qt::RichText); + msgBox.setWindowTitle(tr("Extensions Installation Failed")); + if (!saveFileRes) //failed to save file on disk + msgBox.setText(tr("Unable to save the extension file on disk")); + else //failed to verify package, so size or hash sum is incorrect + msgBox.setText(tr("Unable to download, save and verify the package.\nCheck your internet connection.")); + msgBox.exec(); + } + else { + bool result = mscore->importExtension(localPath); + if (result) { + QFile::remove(localPath); + button->setText(tr("Updated")); + // find uninstall button and make it visible + int rowId = button->property("rowId").toInt(); + QPushButton* uninstallButton = static_cast(extensionsTable->indexWidget(extensionsTable->model()->index(rowId, 4))); + uninstallButton->setDisabled(false); + } + else { + button->setText(tr("Failed, try again")); + button->setEnabled(1); + } + } + } + +//--------------------------------------------------------- +// uninstallExtension +//--------------------------------------------------------- + +void ResourceManager::uninstallExtension() + { + QPushButton* uninstallButton = static_cast(sender()); + QString extensionId = uninstallButton->property("extensionId").toString(); + if (mscore->uninstallExtension(extensionId)) { + // find uninstall button and make it visible + int rowId = uninstallButton->property("rowId").toInt(); + QPushButton* installButton = static_cast(extensionsTable->indexWidget(extensionsTable->model()->index(rowId, 3))); + installButton->setText("Install"); + installButton->setDisabled(false); + uninstallButton->setDisabled(true); + } + } + +//--------------------------------------------------------- +// verifyFile +//--------------------------------------------------------- bool ResourceManager::verifyFile(QString path, QString hash) { diff --git a/mscore/resourceManager.h b/mscore/resourceManager.h index 228a90a30d..16b4791e5b 100644 --- a/mscore/resourceManager.h +++ b/mscore/resourceManager.h @@ -23,21 +23,27 @@ class ResourceManager : public QDialog, public Ui::Resource Q_OBJECT virtual void hideEvent(QHideEvent*); -public: - explicit ResourceManager(QWidget *parent = 0); QByteArray txt; void displayLanguages(); - void displayPlugins(); + void displayExtensions(); bool verifyFile(QString path, QString hash); bool verifyLanguageFile(QString filename, QString hash); -private: - QMap buttonMap; // QPushButton -> filename - QMap buttonHashMap;// QPushButton -> hash of the file - QString baseAddr; +public: + explicit ResourceManager(QWidget *parent = 0); + void selectLanguagesTab(); + void selectExtensionsTab(); -public slots: - void download(); + static inline QString baseAddr() { return "http://extensions.musescore.org/2.3/"; } + +private: + QMap languageButtonMap; // QPushButton -> filename + QMap languageButtonHashMap;// QPushButton -> hash of the file + +private slots: + void downloadLanguage(); + void downloadExtension(); + void uninstallExtension(); }; } diff --git a/mscore/resourceManager.ui b/mscore/resourceManager.ui index a4930fd186..b2dff29dc1 100644 --- a/mscore/resourceManager.ui +++ b/mscore/resourceManager.ui @@ -19,6 +19,71 @@ 0 + + + Extensions + + + + + + + 0 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::NoSelection + + + true + + + 2 + + + 5 + + + false + + + + + + Extension + + + + + Version + + + + + File Size + + + + + Install/Update + + + + + Uninstall + + + + + + @@ -57,7 +122,7 @@ true - 10 + 2 4 @@ -67,14 +132,6 @@ - - - - - - - - Language @@ -99,21 +156,6 @@ - - - Plugins - - - - - 0 - 0 - 251 - 341 - - - - diff --git a/mscore/scoreBrowser.cpp b/mscore/scoreBrowser.cpp index b7d1415f23..29a40de6c8 100644 --- a/mscore/scoreBrowser.cpp +++ b/mscore/scoreBrowser.cpp @@ -64,6 +64,7 @@ ScoreBrowser::ScoreBrowser(QWidget* parent) scoreList->layout()->setMargin(0); _noMatchedScoresLabel = new QLabel(tr("There are no templates matching the current search.")); _noMatchedScoresLabel->setHidden(true); + _noMatchedScoresLabel->setObjectName("noMatchedScoresLabel"); scoreList->layout()->addWidget(_noMatchedScoresLabel); connect(preview, SIGNAL(doubleClicked(QString)), SIGNAL(scoreActivated(QString))); if (!_showPreview) @@ -177,8 +178,16 @@ void ScoreBrowser::setScores(QFileInfoList& s) scoreLists.clear(); QVBoxLayout* l = static_cast(scoreList->layout()); - while (l->count()) - l->removeItem(l->itemAt(0)); + QLayoutItem* child; + while (l->count()) { + child = l->takeAt(0); + if (child->widget() != 0) { + if (child->widget()->objectName() == "noMatchedScoresLabel") // do not delete + continue; + delete child->widget(); + } + delete child; + } ScoreListWidget* sl = 0; @@ -271,14 +280,14 @@ void ScoreBrowser::selectLast() ScoreItem* item = static_cast(w->item(w->count()-1)); w->setCurrentItem(item); preview->setScore(item->info()); -} + } //--------------------------------------------------------- // filter // filter which scores are visible based on searchString //--------------------------------------------------------- void ScoreBrowser::filter(const QString &searchString) -{ + { int numCategoriesWithMathingScores = 0; for (ScoreListWidget* list : scoreLists) { @@ -308,7 +317,7 @@ void ScoreBrowser::filter(const QString &searchString) } _noMatchedScoresLabel->setHidden(numCategoriesWithMathingScores > 0); -} + } //--------------------------------------------------------- // scoreChanged diff --git a/mscore/stringutils.cpp b/mscore/stringutils.cpp index 025b20be08..7fd56cab18 100644 --- a/mscore/stringutils.cpp +++ b/mscore/stringutils.cpp @@ -95,4 +95,72 @@ QString stringutils::removeDiacritics(const QString& pre) return result; } +//--------------------------------------------------------- +// convertFileSizeToHumanReadable +//--------------------------------------------------------- + +QString stringutils::convertFileSizeToHumanReadable(const qlonglong& bytes) + { + QString number; + if (bytes < 0x400) {//If less than 1 KB, report in B + number = QLocale::system().toString(bytes); + number.append(" B"); + return number; + } + else { + if (bytes >= 0x400 && bytes < 0x100000) { //If less than 1 MB, report in KB, unless rounded result is 1024 KB, then report in MB + qlonglong result = (bytes + (0x400 / 2)) / 0x400; + if(result < 0x400) { + number = QLocale::system().toString(result); + number.append(" kB"); + return number; + } + else { + qlonglong result = (bytes + (0x100000 / 2)) / 0x100000; + number = QLocale::system().toString(result); + number.append(" MB"); + return number; + } + } + else { + if (bytes >= 0x100000 && bytes < 0x40000000) { //If less than 1 GB, report in MB, unless rounded result is 1024 MB, then report in GB + qlonglong result = (bytes + (0x100000 / 2)) / 0x100000; + if (result < 0x100000) { + number = QLocale::system().toString(result); + number.append(" MB"); + return number; + } + else { + qlonglong result = (bytes + (0x40000000 / 2)) / 0x40000000; + number = QLocale::system().toString(result); + number.append(" GB"); + return number; + } + } + else { + if (bytes >= 0x40000000 && bytes < 0x10000000000) { //If less than 1 TB, report in GB, unless rounded result is 1024 GB, then report in TB + qlonglong result = (bytes + (0x40000000 / 2)) / 0x40000000; + if (result < 0x40000000) { + number = QLocale::system().toString(result); + number.append(" GB"); + return number; + } + else { + qlonglong result = (bytes + (0x10000000000 / 2)) / 0x10000000000; + number = QLocale::system().toString(result); + number.append(" TB"); + return number; + } + } + else { + qlonglong result = (bytes + (0x10000000000 / 2)) / 0x10000000000; //If more than 1 TB, report in TB + number = QLocale::system().toString(result); + number.append(" TB"); + return number; + } + } + } + } + } + } // namespace Ms diff --git a/mscore/stringutils.h b/mscore/stringutils.h index 6328757fd8..7c39ad3925 100644 --- a/mscore/stringutils.h +++ b/mscore/stringutils.h @@ -30,6 +30,7 @@ class stringutils : public QObject public: static QString removeLigatures(const QString& pre); static QString removeDiacritics(const QString& pre); + static QString convertFileSizeToHumanReadable(const qlonglong & bytes); }; } // namespace Ms diff --git a/mscore/synthcontrol.cpp b/mscore/synthcontrol.cpp index 8fd8b0bb32..b6f856850b 100644 --- a/mscore/synthcontrol.cpp +++ b/mscore/synthcontrol.cpp @@ -320,16 +320,7 @@ void SynthControl::storeButtonClicked() qDebug("no score"); return; } - QString s(dataPath + "/synthesizer.xml"); - QFile f(s); - if (!f.open(QIODevice::WriteOnly)) { - qDebug("cannot write synthesizer settings <%s>", qPrintable(s)); - return; - } - XmlWriter xml(0, &f); - xml.header(); - synti->state().write(xml); - + synti->storeState(); storeButton->setEnabled(false); recallButton->setEnabled(false); } diff --git a/mscore/updatechecker.cpp b/mscore/updatechecker.cpp index 964086dea3..5e63e080cd 100644 --- a/mscore/updatechecker.cpp +++ b/mscore/updatechecker.cpp @@ -21,20 +21,35 @@ #include "musescore.h" #include "libmscore/mscore.h" #include "preferences.h" +#include "resourceManager.h" +#include "extension.h" +#include "libmscore/utils.h" namespace Ms { -UpdateChecker::UpdateChecker() +//--------------------------------------------------------- +// default period +//--------------------------------------------------------- + +static int defaultPeriod() + { + int result = 24; + if(qApp->applicationName() == "MuseScore2"){ //avoid nightly cymbals + if (MuseScore::unstable()) + result = 24; + else + result = 24; // yes, it's again the same but let's keep the logic for now + } + return result; + } + +UpdateChecker::UpdateChecker(QObject* parent) + : UpdateCheckerBase(parent) { manager = new QNetworkAccessManager(this); connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(onRequestFinished(QNetworkReply*))); } -UpdateChecker::~UpdateChecker() - { - delete manager; - } - void UpdateChecker::onRequestFinished(QNetworkReply* reply) { if (reply->error() != QNetworkReply::NoError) { @@ -54,7 +69,6 @@ void UpdateChecker::onRequestFinished(QNetworkReply* reply) QString downloadUrl; QString infoUrl; QString description; - QString releaseType; while (!reader.atEnd() && !reader.hasError()) { QXmlStreamReader::TokenType token = reader.readNext(); @@ -113,6 +127,16 @@ QString UpdateChecker::parseText(QXmlStreamReader& reader) return result; } +bool UpdateChecker::getUpdatePrefValue() + { + return preferences.getBool(PREF_UI_APP_STARTUP_CHECKUPDATE); + } + +QString UpdateChecker::getUpdatePrefString() + { + return "lastUpdateDate"; + } + void UpdateChecker::check(QString currentVersion, bool m) { manual = m; @@ -135,45 +159,110 @@ void UpdateChecker::check(QString currentVersion, bool m) qDebug("release type: %s", release.toLatin1().constData()); if (!os.isEmpty() && !release.isEmpty() && release != "nightly") { _currentVersion = currentVersion; - manager->get(QNetworkRequest(QUrl("http://update.musescore.org/update_"+os +"_" + release +".xml"))); + manager->get(QNetworkRequest(QUrl("http://update.musescore.org/update_" + os +"_" + release +".xml"))); } } -//--------------------------------------------------------- -// default period -//--------------------------------------------------------- - -int UpdateChecker::defaultPeriod() +UpdateCheckerBase::UpdateCheckerBase(QObject* parent) + : QObject(parent) { - int result = 24; - if (qApp->applicationName() == "MuseScore3") { //avoid nightly cymbals - if (MuseScore::unstable()) - result = 24; - else - result = 24; // yes, it's again the same but let's keep the logic for now - } - return result; + } //--------------------------------------------------------- // default hasToCheck //--------------------------------------------------------- -bool UpdateChecker::hasToCheck() +bool UpdateCheckerBase::hasToCheck() { - if (!preferences.getBool(PREF_UI_APP_STARTUP_CHECKUPDATE)) + if(!getUpdatePrefValue()) return false; + QSettings s; s.beginGroup("Update"); QDateTime now = QDateTime::currentDateTime(); - QDateTime lastUpdate = s.value("lastUpdateDate", now).value(); + QDateTime lastUpdate = s.value(getUpdatePrefString(), now).value(); if (MScore::debugMode) { - qDebug("preferences.checkUpdateStartup: %d" , preferences.getBool(PREF_UI_APP_STARTUP_CHECKUPDATE)); - qDebug("lastupdate: %s", qPrintable(lastUpdate.toString("dd.MM.yyyy hh:mm:ss.zzz"))); + qDebug(QString("preferences." + getUpdatePrefString() + ": %d").toStdString().c_str() , getUpdatePrefValue()); + qDebug(QString("last update for " + getUpdatePrefString() + ": %s").toStdString().c_str(), qPrintable(lastUpdate.toString("dd.MM.yyyy hh:mm:ss.zzz"))); } s.endGroup(); return now == lastUpdate || now > lastUpdate.addSecs(3600 * defaultPeriod()) ; } + +ExtensionsUpdateChecker::ExtensionsUpdateChecker(QObject* parent) + : UpdateCheckerBase(parent) + { + + } + +void ExtensionsUpdateChecker::check() + { + DownloadUtils *js = new DownloadUtils(); + js->setTarget(ResourceManager::baseAddr() + "extensions/details.json"); + js->download(); + QByteArray json = js->returnData(); + + // parse the json file + QJsonParseError err; + QJsonDocument result = QJsonDocument::fromJson(json, &err); + if (err.error != QJsonParseError::NoError || !result.isObject()) { + qDebug("An error occurred during parsing"); + return; + } + + QStringList extensions = result.object().keys(); + for (QString key : extensions) { + if (!result.object().value(key).isObject()) + continue; + QJsonObject value = result.object().value(key).toObject(); + QString version = value.value("version").toString(); + + // get the installed version of the extension if any + if (Extension::isInstalled(key)) { + QString installedVersion = Extension::getLatestVersion(key); + if (compareVersion(installedVersion, version)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Extension Updates Available")); + msgBox.setText(tr("One or more installed extensions have updates available in Help / Resource Manager...")); + msgBox.setTextFormat(Qt::RichText); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.setEscapeButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + switch (ret) { + case QMessageBox::Ok: { + ResourceManager r(static_cast(this->parent())); + r.selectExtensionsTab(); + r.exec(); + break; + } + case QMessageBox::Cancel: { + break; + } + default: + qWarning() << "undefined action in ExtensionsUpdateChecker::check" << ret; + } + QSettings s; + s.beginGroup("Update"); + s.setValue(getUpdatePrefString(), QDateTime::currentDateTime()); + s.endGroup(); + break; + } + } + } + } + +bool ExtensionsUpdateChecker::getUpdatePrefValue() + { + return preferences.getBool(PREF_UI_APP_STARTUP_CHECK_EXTENSIONS_UPDATE); + } + +QString ExtensionsUpdateChecker::getUpdatePrefString() + { + return "lastExtensionsUpdateDate"; + } + } diff --git a/mscore/updatechecker.h b/mscore/updatechecker.h index d758d30366..1df1f524b7 100644 --- a/mscore/updatechecker.h +++ b/mscore/updatechecker.h @@ -25,8 +25,19 @@ namespace Ms { //--------------------------------------------------------- // UpdateChecker //--------------------------------------------------------- +class UpdateCheckerBase: public QObject { + Q_OBJECT -class UpdateChecker : public QObject{ +public: + UpdateCheckerBase(QObject* parent); + virtual bool hasToCheck(); + +private: + virtual bool getUpdatePrefValue() = 0; + virtual QString getUpdatePrefString() = 0; + }; + +class UpdateChecker : public UpdateCheckerBase { Q_OBJECT QNetworkAccessManager* manager; @@ -37,20 +48,29 @@ class UpdateChecker : public QObject{ public: void check(QString,bool); - static bool hasToCheck(); + public slots: void onRequestFinished(QNetworkReply*); private: QString parseText(QXmlStreamReader&); - static int defaultPeriod(); - static int computeVersion(QString); + virtual bool getUpdatePrefValue(); + virtual QString getUpdatePrefString(); public: - UpdateChecker(); - ~UpdateChecker(); + UpdateChecker(QObject* parent); }; +class ExtensionsUpdateChecker : public UpdateCheckerBase { + Q_OBJECT +public: + ExtensionsUpdateChecker(QObject* parent); + + void check(); +private: + virtual bool getUpdatePrefValue(); + virtual QString getUpdatePrefString(); + }; } #endif // UPDATECHECKER_H diff --git a/mscore/workspace.cpp b/mscore/workspace.cpp index 3b70ebd1e6..7c73790e21 100644 --- a/mscore/workspace.cpp +++ b/mscore/workspace.cpp @@ -27,6 +27,7 @@ #include "preferences.h" #include "palette.h" #include "palettebox.h" +#include "extension.h" namespace Ms { @@ -458,9 +459,24 @@ void Workspace::save() QList& Workspace::workspaces() { if (!workspacesRead) { + // Remove all workspaces but Basic and Advanced + QMutableListIterator i(_workspaces); + int index = 0; + while (i.hasNext()) { + Workspace* w = i.next(); + if (index >= 2) { + delete w; + i.remove(); + } + index++; + } QStringList path; path << mscoreGlobalShare + "workspaces"; path << dataPath + "/workspaces"; + + QStringList extensionsDir = Extension::getDirectoriesByType(Extension::workspacesDir); + path.append(extensionsDir); + QStringList nameFilters; nameFilters << "*.workspace"; @@ -491,6 +507,16 @@ QList& Workspace::workspaces() return _workspaces; } +//--------------------------------------------------------- +// refreshWorkspaces +//--------------------------------------------------------- + +QList& Workspace::refreshWorkspaces() + { + workspacesRead = false; + return workspaces(); + } + //--------------------------------------------------------- // createNewWorkspace //--------------------------------------------------------- diff --git a/mscore/workspace.h b/mscore/workspace.h index 60016db9bf..bd72f5b514 100644 --- a/mscore/workspace.h +++ b/mscore/workspace.h @@ -68,6 +68,7 @@ class Workspace : public QObject { static Workspace* createNewWorkspace(const QString& name); static bool workspacesRead; static void writeBuiltinWorkspace(); + static QList& refreshWorkspaces(); }; } #endif diff --git a/mtest/CMakeLists.txt b/mtest/CMakeLists.txt index ac3da30e23..223886adfa 100644 --- a/mtest/CMakeLists.txt +++ b/mtest/CMakeLists.txt @@ -101,6 +101,7 @@ add_library( ${PROJECT_SOURCE_DIR}/thirdparty/beatroot/AgentList.cpp # Required by importmidi.cpp ${PROJECT_SOURCE_DIR}/thirdparty/beatroot/BeatTracker.cpp # Required by importmidi.cpp ${PROJECT_SOURCE_DIR}/thirdparty/beatroot/Induction.cpp # Required by importmidi.cpp + ${PROJECT_SOURCE_DIR}/mscore/extension.cpp # required by zerberus tests ${OMR_SRC} omr ) @@ -188,6 +189,7 @@ subdirs ( libmscore/transpose libmscore/tuplet # libmscore/text work in progress... + libmscore/utils importmidi capella biab diff --git a/mtest/libmscore/utils/CMakeLists.txt b/mtest/libmscore/utils/CMakeLists.txt new file mode 100644 index 0000000000..4b888ff777 --- /dev/null +++ b/mtest/libmscore/utils/CMakeLists.txt @@ -0,0 +1,17 @@ +#============================================================================= +# MuseScore +# Music Composition & Notation +# $Id:$ +# +# Copyright (C) 2011 Werner Schweer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 +# as published by the Free Software Foundation and appearing in +# the file LICENSE.GPL +#============================================================================= + +set(TARGET tst_utils) + +include(${PROJECT_SOURCE_DIR}/mtest/cmake.inc) + diff --git a/mtest/libmscore/utils/tst_utils.cpp b/mtest/libmscore/utils/tst_utils.cpp new file mode 100644 index 0000000000..ff0eead0ed --- /dev/null +++ b/mtest/libmscore/utils/tst_utils.cpp @@ -0,0 +1,68 @@ +//============================================================================= +// MuseScore +// Music Composition & Notation +// $Id:$ +// +// Copyright (C) 2018 Werner Schweer +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 +// as published by the Free Software Foundation and appearing in +// the file LICENCE.GPL +//============================================================================= + +#include + +#include "libmscore/utils.h" +#include "mtest/testutils.h" + +#define DIR QString("libmscore/utils/") + +using namespace Ms; + +//--------------------------------------------------------- +// TestNote +//--------------------------------------------------------- + +class TestUtils : public QObject, public MTest + { + Q_OBJECT + + private slots: + void initTestCase(); + void tst_compareVersion(); + }; + +//--------------------------------------------------------- +// initTestCase +//--------------------------------------------------------- + +void TestUtils::initTestCase() + { + initMTest(); + } + +//--------------------------------------------------------- +/// test_version +//--------------------------------------------------------- + +void TestUtils::tst_compareVersion() + { + QVERIFY(compareVersion("0.22", "1.0") == true); + QVERIFY(compareVersion("1", "2") == true); + QVERIFY(compareVersion("1.0", "2.0") == true); + QVERIFY(compareVersion("1.14", "1.16") == true); + QVERIFY(compareVersion("1.16", "1.14") == false); + QVERIFY(compareVersion("2.1", "2.0") == false); + QVERIFY(compareVersion("2.0", "2.1") == true); + QVERIFY(compareVersion("2.1.1.2", "2.0") == false); + QVERIFY(compareVersion("2.0", "2.1.1.3") == true); + QVERIFY(compareVersion("2.1.1.2", "2.1.1.3") == true); + QVERIFY(compareVersion("test", "2.1") == true); + QVERIFY(compareVersion("test1", "test") == false); + } + +QTEST_MAIN(TestUtils) + +#include "tst_utils.moc" + diff --git a/mtest/zerberus/comments/CMakeLists.txt b/mtest/zerberus/comments/CMakeLists.txt index 7c88f48e81..28e59b566c 100644 --- a/mtest/zerberus/comments/CMakeLists.txt +++ b/mtest/zerberus/comments/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzcomments zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzcomments zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/envelopes/CMakeLists.txt b/mtest/zerberus/envelopes/CMakeLists.txt index faabd2cbcd..60834dcd36 100644 --- a/mtest/zerberus/envelopes/CMakeLists.txt +++ b/mtest/zerberus/envelopes/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzenvelopes zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzenvelopes zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/global/CMakeLists.txt b/mtest/zerberus/global/CMakeLists.txt index 32f601bf6a..f6911c4dd9 100644 --- a/mtest/zerberus/global/CMakeLists.txt +++ b/mtest/zerberus/global/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzglobal zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzglobal zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/includes/CMakeLists.txt b/mtest/zerberus/includes/CMakeLists.txt index 4b078342a5..a48fe0c07f 100644 --- a/mtest/zerberus/includes/CMakeLists.txt +++ b/mtest/zerberus/includes/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzincludes zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzincludes zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/inputControls/CMakeLists.txt b/mtest/zerberus/inputControls/CMakeLists.txt index c04673e4a3..a206ad1027 100644 --- a/mtest/zerberus/inputControls/CMakeLists.txt +++ b/mtest/zerberus/inputControls/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzinputcontrols zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzinputcontrols zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/loop/CMakeLists.txt b/mtest/zerberus/loop/CMakeLists.txt index b731e5fe5b..4bc7f147e6 100644 --- a/mtest/zerberus/loop/CMakeLists.txt +++ b/mtest/zerberus/loop/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzloop zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzloop zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/mtest/zerberus/opcodeparse/CMakeLists.txt b/mtest/zerberus/opcodeparse/CMakeLists.txt index fddf0073d0..2691bf9257 100644 --- a/mtest/zerberus/opcodeparse/CMakeLists.txt +++ b/mtest/zerberus/opcodeparse/CMakeLists.txt @@ -19,4 +19,4 @@ include_directories( ${SNDFILE_INCDIR} ) -target_link_libraries(tst_sfzopcodes zerberus synthesizer audiofile ${SNDFILE_LIB}) +target_link_libraries(tst_sfzopcodes zerberus synthesizer audiofile ${SNDFILE_LIB} testutils) diff --git a/synthesizer/msynthesizer.cpp b/synthesizer/msynthesizer.cpp index 0c81ae08c7..1a1d7384b1 100644 --- a/synthesizer/msynthesizer.cpp +++ b/synthesizer/msynthesizer.cpp @@ -367,6 +367,24 @@ SynthesizerState MasterSynthesizer::state() const return ss; } +//--------------------------------------------------------- +// storeState +//--------------------------------------------------------- + +bool MasterSynthesizer::storeState() + { + QString s(dataPath + "/synthesizer.xml"); + QFile f(s); + if (!f.open(QIODevice::WriteOnly)) { + qDebug("cannot write synthesizer settings <%s>", qPrintable(s)); + return false; + } + XmlWriter xml(0, &f); + xml.header(); + state().write(xml); + return true; + } + //--------------------------------------------------------- // setGain //--------------------------------------------------------- diff --git a/synthesizer/msynthesizer.h b/synthesizer/msynthesizer.h index 3e0adb3e18..3e13851a2b 100644 --- a/synthesizer/msynthesizer.h +++ b/synthesizer/msynthesizer.h @@ -102,6 +102,8 @@ class MasterSynthesizer : public QObject { float gain() const { return _gain; } float boost() const { return _boost; } void setBoost(float v) { _boost = v; } + + bool storeState(); }; } diff --git a/thirdparty/qzip/qzip.cpp b/thirdparty/qzip/qzip.cpp index 187d740fc0..032b779c20 100644 --- a/thirdparty/qzip/qzip.cpp +++ b/thirdparty/qzip/qzip.cpp @@ -1,39 +1,37 @@ /**************************************************************************** ** -** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtGui module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ -** GNU Lesser General Public License Usage -** This file may be used under the terms of the GNU Lesser General Public -** License version 2.1 as published by the Free Software Foundation and -** appearing in the file LICENSE.LGPL included in the packaging of this -** file. Please review the following information to ensure the GNU Lesser -** General Public License version 2.1 requirements will be met: -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. ** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU General -** Public License version 3.0 as published by the Free Software Foundation -** and appearing in the file LICENSE.GPL included in the packaging of this -** file. Please review the following information to ensure the GNU General -** Public License version 3.0 requirements will be met: -** http://www.gnu.org/copyleft/gpl.html. -** -** Other Usage -** Alternatively, this file may be used in accordance with the terms and -** conditions contained in a signed written agreement between you and Nokia. -** -** -** -** +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** @@ -47,47 +45,9 @@ #include -#if defined(Q_OS_WIN) or defined(Q_OS_ANDROID) -# undef S_IFREG -# define S_IFREG 0100000 -# ifndef S_IFDIR -# define S_IFDIR 0040000 -# endif -# ifndef S_ISDIR -# define S_ISDIR(x) ((x) & S_IFDIR) > 0 -# endif -# ifndef S_ISREG -# define S_ISREG(x) ((x) & 0170000) == S_IFREG -# endif -# define S_IFLNK 020000 -# define S_ISLNK(x) ((x) & S_IFLNK) > 0 -# ifndef S_IRUSR -# define S_IRUSR 0400 -# endif -# ifndef S_IWUSR -# define S_IWUSR 0200 -# endif -# ifndef S_IXUSR -# define S_IXUSR 0100 -# endif -# define S_IRGRP 0040 -# define S_IWGRP 0020 -# define S_IXGRP 0010 -# define S_IROTH 0004 -# define S_IWOTH 0002 -# define S_IXOTH 0001 -#else -// # ifndef S_IFDIR -// # define S_IFDIR 0040000 -// # endif -# ifndef S_ISDIR -# define S_ISDIR(x) ((x) & S_IFDIR) > 0 -# endif -# ifndef S_ISREG -# define S_ISREG(x) ((x) & 0170000) == S_IFREG -# endif -# define S_ISLNK(x) ((x) & S_IFLNK) > 0 -#endif +// Zip standard version for archives handled by this API +// (actually, the only basic support of this version is implemented but it is enough for now) +#define ZIP_VERSION 20 #if 0 #define ZDEBUG qDebug @@ -161,42 +121,12 @@ static void writeMSDosDate(uchar *dest, const QDateTime& dt) } } -static quint32 permissionsToMode(QFile::Permissions perms) -{ - quint32 mode = 0; - if (perms & QFile::ReadOwner) - mode |= S_IRUSR; - if (perms & QFile::WriteOwner) - mode |= S_IWUSR; - if (perms & QFile::ExeOwner) - mode |= S_IXUSR; - if (perms & QFile::ReadUser) - mode |= S_IRUSR; - if (perms & QFile::WriteUser) - mode |= S_IWUSR; - if (perms & QFile::ExeUser) - mode |= S_IXUSR; - if (perms & QFile::ReadGroup) - mode |= S_IRGRP; - if (perms & QFile::WriteGroup) - mode |= S_IWGRP; - if (perms & QFile::ExeGroup) - mode |= S_IXGRP; - if (perms & QFile::ReadOther) - mode |= S_IROTH; - if (perms & QFile::WriteOther) - mode |= S_IWOTH; - if (perms & QFile::ExeOther) - mode |= S_IXOTH; - return mode; -} - static int inflate(Bytef *dest, ulong *destLen, const Bytef *source, ulong sourceLen) { z_stream stream; int err; - stream.next_in = (Bytef*)source; + stream.next_in = const_cast(source); stream.avail_in = (uInt)sourceLen; if ((uLong)stream.avail_in != sourceLen) return Z_BUF_ERROR; @@ -231,7 +161,7 @@ static int deflate (Bytef *dest, ulong *destLen, const Bytef *source, ulong sour z_stream stream; int err; - stream.next_in = (Bytef*)source; + stream.next_in = const_cast(source); stream.avail_in = (uInt)sourceLen; stream.next_out = dest; stream.avail_out = (uInt)*destLen; @@ -255,36 +185,86 @@ static int deflate (Bytef *dest, ulong *destLen, const Bytef *source, ulong sour return err; } + +namespace WindowsFileAttributes { +enum { + Dir = 0x10, // FILE_ATTRIBUTE_DIRECTORY + File = 0x80, // FILE_ATTRIBUTE_NORMAL + TypeMask = 0x90, + + ReadOnly = 0x01, // FILE_ATTRIBUTE_READONLY + PermMask = 0x01 +}; +} + +namespace UnixFileAttributes { +enum { + Dir = 0040000, // __S_IFDIR + File = 0100000, // __S_IFREG + SymLink = 0120000, // __S_IFLNK + TypeMask = 0170000, // __S_IFMT + + ReadUser = 0400, // __S_IRUSR + WriteUser = 0200, // __S_IWUSR + ExeUser = 0100, // __S_IXUSR + ReadGroup = 0040, // __S_IRGRP + WriteGroup = 0020, // __S_IWGRP + ExeGroup = 0010, // __S_IXGRP + ReadOther = 0004, // __S_IROTH + WriteOther = 0002, // __S_IWOTH + ExeOther = 0001, // __S_IXOTH + PermMask = 0777 +}; +} + static QFile::Permissions modeToPermissions(quint32 mode) { QFile::Permissions ret; - if (mode & S_IRUSR) - ret |= QFile::ReadOwner; - if (mode & S_IWUSR) - ret |= QFile::WriteOwner; - if (mode & S_IXUSR) - ret |= QFile::ExeOwner; - if (mode & S_IRUSR) - ret |= QFile::ReadUser; - if (mode & S_IWUSR) - ret |= QFile::WriteUser; - if (mode & S_IXUSR) - ret |= QFile::ExeUser; - if (mode & S_IRGRP) + if (mode & UnixFileAttributes::ReadUser) + ret |= QFile::ReadOwner | QFile::ReadUser; + if (mode & UnixFileAttributes::WriteUser) + ret |= QFile::WriteOwner | QFile::WriteUser; + if (mode & UnixFileAttributes::ExeUser) + ret |= QFile::ExeOwner | QFile::ExeUser; + if (mode & UnixFileAttributes::ReadGroup) ret |= QFile::ReadGroup; - if (mode & S_IWGRP) + if (mode & UnixFileAttributes::WriteGroup) ret |= QFile::WriteGroup; - if (mode & S_IXGRP) + if (mode & UnixFileAttributes::ExeGroup) ret |= QFile::ExeGroup; - if (mode & S_IROTH) + if (mode & UnixFileAttributes::ReadOther) ret |= QFile::ReadOther; - if (mode & S_IWOTH) + if (mode & UnixFileAttributes::WriteOther) ret |= QFile::WriteOther; - if (mode & S_IXOTH) + if (mode & UnixFileAttributes::ExeOther) ret |= QFile::ExeOther; return ret; } +static quint32 permissionsToMode(QFile::Permissions perms) +{ + quint32 mode = 0; + if (perms & (QFile::ReadOwner | QFile::ReadUser)) + mode |= UnixFileAttributes::ReadUser; + if (perms & (QFile::WriteOwner | QFile::WriteUser)) + mode |= UnixFileAttributes::WriteUser; + if (perms & (QFile::ExeOwner | QFile::ExeUser)) + mode |= UnixFileAttributes::WriteUser; + if (perms & QFile::ReadGroup) + mode |= UnixFileAttributes::ReadGroup; + if (perms & QFile::WriteGroup) + mode |= UnixFileAttributes::WriteGroup; + if (perms & QFile::ExeGroup) + mode |= UnixFileAttributes::ExeGroup; + if (perms & QFile::ReadOther) + mode |= UnixFileAttributes::ReadOther; + if (perms & QFile::WriteOther) + mode |= UnixFileAttributes::WriteOther; + if (perms & QFile::ExeOther) + mode |= UnixFileAttributes::ExeOther; + return mode; +} + static QDateTime readMSDosDate(const uchar *src) { uint dosDate = readUInt(src); @@ -300,6 +280,71 @@ static QDateTime readMSDosDate(const uchar *src) return QDateTime(QDate(tm_year, tm_mon, tm_mday), QTime(tm_hour, tm_min, tm_sec)); } +// for details, see http://www.pkware.com/documents/casestudies/APPNOTE.TXT + +enum HostOS { + HostFAT = 0, + HostAMIGA = 1, + HostVMS = 2, // VAX/VMS + HostUnix = 3, + HostVM_CMS = 4, + HostAtari = 5, // what if it's a minix filesystem? [cjh] + HostHPFS = 6, // filesystem used by OS/2 (and NT 3.x) + HostMac = 7, + HostZ_System = 8, + HostCPM = 9, + HostTOPS20 = 10, // pkzip 2.50 NTFS + HostNTFS = 11, // filesystem used by Windows NT + HostQDOS = 12, // SMS/QDOS + HostAcorn = 13, // Archimedes Acorn RISC OS + HostVFAT = 14, // filesystem used by Windows 95, NT + HostMVS = 15, + HostBeOS = 16, // hybrid POSIX/database filesystem + HostTandem = 17, + HostOS400 = 18, + HostOSX = 19 +}; +Q_DECLARE_TYPEINFO(HostOS, Q_PRIMITIVE_TYPE); + +enum GeneralPurposeFlag { + Encrypted = 0x01, + AlgTune1 = 0x02, + AlgTune2 = 0x04, + HasDataDescriptor = 0x08, + PatchedData = 0x20, + StrongEncrypted = 0x40, + Utf8Names = 0x0800, + CentralDirectoryEncrypted = 0x2000 +}; +Q_DECLARE_TYPEINFO(GeneralPurposeFlag, Q_PRIMITIVE_TYPE); + +enum CompressionMethod { + CompressionMethodStored = 0, + CompressionMethodShrunk = 1, + CompressionMethodReduced1 = 2, + CompressionMethodReduced2 = 3, + CompressionMethodReduced3 = 4, + CompressionMethodReduced4 = 5, + CompressionMethodImploded = 6, + CompressionMethodReservedTokenizing = 7, // reserved for tokenizing + CompressionMethodDeflated = 8, + CompressionMethodDeflated64 = 9, + CompressionMethodPKImploding = 10, + + CompressionMethodBZip2 = 12, + + CompressionMethodLZMA = 14, + + CompressionMethodTerse = 18, + CompressionMethodLz77 = 19, + + CompressionMethodJpeg = 96, + CompressionMethodWavPack = 97, + CompressionMethodPPMd = 98, + CompressionMethodWzAES = 99 +}; +Q_DECLARE_TYPEINFO(CompressionMethod, Q_PRIMITIVE_TYPE); + struct LocalFileHeader { uchar signature[4]; // 0x04034b50 @@ -313,6 +358,7 @@ struct LocalFileHeader uchar file_name_length[2]; uchar extra_field_length[2]; }; +Q_DECLARE_TYPEINFO(LocalFileHeader, Q_PRIMITIVE_TYPE); struct DataDescriptor { @@ -320,8 +366,9 @@ struct DataDescriptor uchar compressed_size[4]; uchar uncompressed_size[4]; }; +Q_DECLARE_TYPEINFO(DataDescriptor, Q_PRIMITIVE_TYPE); -struct MCentralFileHeader +struct CentralFileHeader { uchar signature[4]; // 0x02014b50 uchar version_made[2]; @@ -341,6 +388,7 @@ struct MCentralFileHeader uchar offset_local_header[4]; LocalFileHeader toLocalHeader() const; }; +Q_DECLARE_TYPEINFO(CentralFileHeader, Q_PRIMITIVE_TYPE); struct EndOfDirectory { @@ -353,46 +401,16 @@ struct EndOfDirectory uchar dir_start_offset[4]; uchar comment_length[2]; }; +Q_DECLARE_TYPEINFO(EndOfDirectory, Q_PRIMITIVE_TYPE); struct FileHeader { - MCentralFileHeader h; + CentralFileHeader h; QByteArray file_name; QByteArray extra_field; QByteArray file_comment; }; - -MQZipReader::FileInfo::FileInfo() - : isDir(false), isFile(false), isSymLink(false), crc32(0), size(0) -{ -} - -MQZipReader::FileInfo::~FileInfo() -{ -} - -MQZipReader::FileInfo::FileInfo(const FileInfo &other) -{ - operator=(other); -} - -MQZipReader::FileInfo& MQZipReader::FileInfo::operator=(const FileInfo &other) -{ - filePath = other.filePath; - isDir = other.isDir; - isFile = other.isFile; - isSymLink = other.isSymLink; - permissions = other.permissions; - crc32 = other.crc32; - size = other.size; - lastModified = other.lastModified; - return *this; -} - -bool MQZipReader::FileInfo::isValid() const -{ - return isDir || isFile || isSymLink; -} +Q_DECLARE_TYPEINFO(FileHeader, Q_MOVABLE_TYPE); class MQZipPrivate { @@ -408,28 +426,78 @@ public: delete device; } - void fillFileInfo(int index, MQZipReader::FileInfo &fileInfo) const; + MQZipReader::FileInfo fillFileInfo(int index) const; QIODevice *device; bool ownDevice; bool dirtyFileTree; - QList fileHeaders; + QVector fileHeaders; QByteArray comment; uint start_of_directory; }; -void MQZipPrivate::fillFileInfo(int index, MQZipReader::FileInfo &fileInfo) const +MQZipReader::FileInfo MQZipPrivate::fillFileInfo(int index) const { + MQZipReader::FileInfo fileInfo; FileHeader header = fileHeaders.at(index); - fileInfo.filePath = QString::fromLocal8Bit(header.file_name); - const quint32 mode = (qFromLittleEndian(&header.h.external_file_attributes[0]) >> 16) & 0xFFFF; - fileInfo.isDir = S_ISDIR(mode); - fileInfo.isFile = S_ISREG(mode); - fileInfo.isSymLink = S_ISLNK(mode); - fileInfo.permissions = modeToPermissions(mode); - fileInfo.crc32 = readUInt(header.h.crc_32); + quint32 mode = readUInt(header.h.external_file_attributes); + const HostOS hostOS = HostOS(readUShort(header.h.version_made) >> 8); + switch (hostOS) { + case HostUnix: + mode = (mode >> 16) & 0xffff; + switch (mode & UnixFileAttributes::TypeMask) { + case UnixFileAttributes::SymLink: + fileInfo.isSymLink = true; + break; + case UnixFileAttributes::Dir: + fileInfo.isDir = true; + break; + case UnixFileAttributes::File: + default: // ### just for the case; should we warn? + fileInfo.isFile = true; + break; + } + fileInfo.permissions = modeToPermissions(mode); + break; + case HostFAT: + case HostNTFS: + case HostHPFS: + case HostVFAT: + switch (mode & WindowsFileAttributes::TypeMask) { + case WindowsFileAttributes::Dir: + fileInfo.isDir = true; + break; + case WindowsFileAttributes::File: + default: + fileInfo.isFile = true; + break; + } + fileInfo.permissions |= QFile::ReadOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther; + if ((mode & WindowsFileAttributes::ReadOnly) == 0) + fileInfo.permissions |= QFile::WriteOwner | QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther; + if (fileInfo.isDir) + fileInfo.permissions |= QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther; + break; + default: + qWarning("QZip: Zip entry format at %d is not supported.", index); + return fileInfo; // we don't support anything else + } + + ushort general_purpose_bits = readUShort(header.h.general_purpose_bits); + // if bit 11 is set, the filename and comment fields must be encoded using UTF-8 + const bool inUtf8 = (general_purpose_bits & Utf8Names) != 0; + fileInfo.filePath = inUtf8 ? QString::fromUtf8(header.file_name) : QString::fromLocal8Bit(header.file_name); + fileInfo.crc = readUInt(header.h.crc_32); fileInfo.size = readUInt(header.h.uncompressed_size); fileInfo.lastModified = readMSDosDate(header.h.last_mod_file); + + // fix the file path, if broken (convert separators, eat leading and trailing ones) + fileInfo.filePath = QDir::fromNativeSeparators(fileInfo.filePath); + while (!fileInfo.filePath.isEmpty() && (fileInfo.filePath.at(0) == QLatin1Char('.') || fileInfo.filePath.at(0) == QLatin1Char('/'))) + fileInfo.filePath = fileInfo.filePath.mid(1); + while (!fileInfo.filePath.isEmpty() && fileInfo.filePath.at(fileInfo.filePath.size() - 1) == QLatin1Char('/')) + fileInfo.filePath.chop(1); + return fileInfo; } class MQZipReaderPrivate : public MQZipPrivate @@ -465,7 +533,7 @@ public: void addEntry(EntryType type, const QString &fileName, const QByteArray &contents); }; -LocalFileHeader MCentralFileHeader::toLocalHeader() const +LocalFileHeader CentralFileHeader::toLocalHeader() const { LocalFileHeader h; writeUInt(h.signature, 0x04034b50); @@ -500,7 +568,7 @@ void MQZipReaderPrivate::scanFiles() uchar tmp[4]; device->read((char *)tmp, 4); if (readUInt(tmp) != 0x04034b50) { - qWarning() << "QZip: not a zip file!"; + qWarning("QZip: not a zip file!"); return; } @@ -510,9 +578,9 @@ void MQZipReaderPrivate::scanFiles() int num_dir_entries = 0; EndOfDirectory eod; while (start_of_directory == -1) { - int pos = device->size() - sizeof(EndOfDirectory) - i; + const int pos = device->size() - int(sizeof(EndOfDirectory)) - i; if (pos < 0 || i > 65535) { - qWarning() << "QZip: EndOfDirectory not found"; + qWarning("QZip: EndOfDirectory not found"); return; } @@ -529,39 +597,39 @@ void MQZipReaderPrivate::scanFiles() ZDEBUG("start_of_directory at %d, num_dir_entries=%d", start_of_directory, num_dir_entries); int comment_length = readUShort(eod.comment_length); if (comment_length != i) - qWarning() << "QZip: failed to parse zip file."; + qWarning("QZip: failed to parse zip file."); comment = device->read(qMin(comment_length, i)); device->seek(start_of_directory); for (i = 0; i < num_dir_entries; ++i) { FileHeader header; - int read = device->read((char *) &header.h, sizeof(MCentralFileHeader)); - if (read < (int)sizeof(MCentralFileHeader)) { - qWarning() << "QZip: Failed to read complete header, index may be incomplete"; + int read = device->read((char *) &header.h, sizeof(CentralFileHeader)); + if (read < (int)sizeof(CentralFileHeader)) { + qWarning("QZip: Failed to read complete header, index may be incomplete"); break; } if (readUInt(header.h.signature) != 0x02014b50) { - qWarning() << "QZip: invalid header signature, index may be incomplete"; + qWarning("QZip: invalid header signature, index may be incomplete"); break; } int l = readUShort(header.h.file_name_length); header.file_name = device->read(l); if (header.file_name.length() != l) { - qWarning() << "QZip: Failed to read filename from zip index, index may be incomplete"; + qWarning("QZip: Failed to read filename from zip index, index may be incomplete"); break; } l = readUShort(header.h.extra_field_length); header.extra_field = device->read(l); if (header.extra_field.length() != l) { - qWarning() << "QZip: Failed to read extra field in zip file, skipping file, index may be incomplete"; + qWarning("QZip: Failed to read extra field in zip file, skipping file, index may be incomplete"); break; } l = readUShort(header.h.file_comment_length); header.file_comment = device->read(l); if (header.file_comment.length() != l) { - qWarning() << "QZip: Failed to read read file comment, index may be incomplete"; + qWarning("QZip: Failed to read read file comment, index may be incomplete"); break; } @@ -573,7 +641,7 @@ void MQZipReaderPrivate::scanFiles() void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const QByteArray &contents/*, QFile::Permissions permissions, QZip::Method m*/) { #ifndef NDEBUG - static const char *entryTypes[] = { + static const char *const entryTypes[] = { "directory", "file ", "symlink " }; @@ -596,15 +664,15 @@ void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const } FileHeader header; - memset(&header.h, 0, sizeof(MCentralFileHeader)); + memset(&header.h, 0, sizeof(CentralFileHeader)); writeUInt(header.h.signature, 0x02014b50); - writeUShort(header.h.version_needed, 0x14); + writeUShort(header.h.version_needed, ZIP_VERSION); writeUInt(header.h.uncompressed_size, contents.length()); writeMSDosDate(header.h.last_mod_file, QDateTime::currentDateTime()); QByteArray data = contents; if (compression == MQZipWriter::AlwaysCompress) { - writeUShort(header.h.compression_method, 8); + writeUShort(header.h.compression_method, CompressionMethodDeflated); ulong len = contents.length(); // shamelessly copied form zlib @@ -634,22 +702,40 @@ void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const crc_32 = ::crc32(crc_32, (const uchar *)contents.constData(), contents.length()); writeUInt(header.h.crc_32, crc_32); - header.file_name = fileName.toUtf8(); + // if bit 11 is set, the filename and comment fields must be encoded using UTF-8 + ushort general_purpose_bits = Utf8Names; // always use utf-8 + writeUShort(header.h.general_purpose_bits, general_purpose_bits); + + const bool inUtf8 = (general_purpose_bits & Utf8Names) != 0; + header.file_name = inUtf8 ? fileName.toUtf8() : fileName.toLocal8Bit(); if (header.file_name.size() > 0xffff) { - qWarning("QZip: Filename too long, chopping it to 65535 characters"); - header.file_name = header.file_name.left(0xffff); + qWarning("QZip: Filename is too long, chopping it to 65535 bytes"); + header.file_name = header.file_name.left(0xffff); // ### don't break the utf-8 sequence, if any + } + if (header.file_comment.size() + header.file_name.size() > 0xffff) { + qWarning("QZip: File comment is too long, chopping it to 65535 bytes"); + header.file_comment.truncate(0xffff - header.file_name.size()); // ### don't break the utf-8 sequence, if any } writeUShort(header.h.file_name_length, header.file_name.length()); //h.extra_field_length[2]; - writeUShort(header.h.version_made, 3 << 8); + writeUShort(header.h.version_made, HostUnix << 8); //uchar internal_file_attributes[2]; //uchar external_file_attributes[4]; quint32 mode = permissionsToMode(permissions); switch (type) { - case File: mode |= S_IFREG; break; - case Directory: mode |= S_IFDIR; break; - case Symlink: mode |= S_IFLNK; break; + case Symlink: + mode |= UnixFileAttributes::SymLink; + break; + case Directory: + mode |= UnixFileAttributes::Dir; + break; + case File: + mode |= UnixFileAttributes::File; + break; + default: + Q_UNREACHABLE(); + break; } writeUInt(header.h.external_file_attributes, mode << 16); writeUInt(header.h.offset_local_header, start_of_directory); @@ -699,8 +785,8 @@ void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const */ /*! - \variable FileInfo::crc32 - The calculated checksum as a crc32 type. + \variable FileInfo::crc + The calculated checksum as a crc type. */ /*! @@ -708,12 +794,6 @@ void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const The total size of the unpacked content. */ -/*! - \variable FileInfo::d - \internal - private pointer. -*/ - /*! \class QZipReader \internal @@ -736,16 +816,17 @@ void MQZipWriterPrivate::addEntry(EntryType type, const QString &fileName, const MQZipReader::MQZipReader(const QString &archive, QIODevice::OpenMode mode) { QScopedPointer f(new QFile(archive)); - f->open(mode); + const bool result = f->open(mode); MQZipReader::Status status; - if (f->error() == QFile::NoError) + const QFileDevice::FileError error = f->error(); + if (result && error == QFile::NoError) { status = NoError; - else { - if (f->error() == QFile::ReadError) + } else { + if (error == QFile::ReadError) status = FileReadError; - else if (f->error() == QFile::OpenError) + else if (error == QFile::OpenError) status = FileOpenError; - else if (f->error() == QFile::PermissionsError) + else if (error == QFile::PermissionsError) status = FilePermissionsError; else status = FileError; @@ -785,7 +866,7 @@ QIODevice* MQZipReader::device() const } /*! - Returns true if the user can read the file; otherwise returns false. + Returns \c true if the user can read the file; otherwise returns \c false. */ bool MQZipReader::isReadable() const { @@ -793,7 +874,7 @@ bool MQZipReader::isReadable() const } /*! - Returns true if the file exists; otherwise returns false. + Returns \c true if the file exists; otherwise returns \c false. */ bool MQZipReader::exists() const { @@ -806,15 +887,14 @@ bool MQZipReader::exists() const /*! Returns the list of files the archive contains. */ -QList MQZipReader::fileInfoList() const +QVector MQZipReader::fileInfoList() const { d->scanFiles(); - QList files; - for (int i = 0; i < d->fileHeaders.size(); ++i) { - MQZipReader::FileInfo fi; - d->fillFileInfo(i, fi); - files.append(fi); - } + QVector files; + const int numFileHeaders = d->fileHeaders.size(); + files.reserve(numFileHeaders); + for (int i = 0; i < numFileHeaders; ++i) + files.append(d->fillFileInfo(i)); return files; } @@ -838,10 +918,9 @@ int MQZipReader::count() const MQZipReader::FileInfo MQZipReader::entryInfoAt(int index) const { d->scanFiles(); - MQZipReader::FileInfo fi; if (index >= 0 && index < d->fileHeaders.count()) - d->fillFileInfo(index, fi); - return fi; + return d->fillFileInfo(index); + return MQZipReader::FileInfo(); } /*! @@ -860,6 +939,13 @@ QByteArray MQZipReader::fileData(const QString &fileName) const FileHeader header = d->fileHeaders.at(i); + ushort version_needed = readUShort(header.h.version_needed); + if (version_needed > ZIP_VERSION) { + qWarning("QZip: .ZIP specification version %d implementationis needed to extract the data.", version_needed); + return QByteArray(); + } + + ushort general_purpose_bits = readUShort(header.h.general_purpose_bits); int compressed_size = readUInt(header.h.compressed_size); int uncompressed_size = readUInt(header.h.uncompressed_size); int start = readUInt(header.h.offset_local_header); @@ -874,13 +960,18 @@ QByteArray MQZipReader::fileData(const QString &fileName) const int compression_method = readUShort(lh.compression_method); //qDebug("file=%s: compressed_size=%d, uncompressed_size=%d", fileName.toLocal8Bit().data(), compressed_size, uncompressed_size); + if ((general_purpose_bits & Encrypted) != 0) { + qWarning("QZip: Unsupported encryption method is needed to extract the data."); + return QByteArray(); + } + //qDebug("file at %lld", d->device->pos()); QByteArray compressed = d->device->read(compressed_size); - if (compression_method == 0) { + if (compression_method == CompressionMethodStored) { // no compression compressed.truncate(uncompressed_size); return compressed; - } else if (compression_method == 8) { + } else if (compression_method == CompressionMethodDeflated) { // Deflate //qDebug("compressed=%d", compressed.size()); compressed.truncate(compressed_size); @@ -890,7 +981,7 @@ QByteArray MQZipReader::fileData(const QString &fileName) const do { baunzip.resize(len); res = inflate((uchar*)baunzip.data(), &len, - (uchar*)compressed.constData(), compressed_size); + (const uchar*)compressed.constData(), compressed_size); switch (res) { case Z_OK: @@ -910,7 +1001,8 @@ QByteArray MQZipReader::fileData(const QString &fileName) const } while (res == Z_BUF_ERROR); return baunzip; } - qWarning() << "QZip: Unknown compression method"; + + qWarning("QZip: Unsupported compression method %d is needed to extract the data.", compression_method); return QByteArray(); } @@ -924,8 +1016,8 @@ bool MQZipReader::extractAll(const QString &destinationDir) const QDir baseDir(destinationDir); // create directories first - QList allFiles = fileInfoList(); - foreach (FileInfo fi, allFiles) { + const QVector allFiles = fileInfoList(); + for (const FileInfo &fi : allFiles) { const QString absPath = destinationDir + QDir::separator() + fi.filePath; if (fi.isDir) { if (!baseDir.mkpath(fi.filePath)) @@ -936,7 +1028,7 @@ bool MQZipReader::extractAll(const QString &destinationDir) const } // set up symlinks - foreach (FileInfo fi, allFiles) { + for (const FileInfo &fi : allFiles) { const QString absPath = destinationDir + QDir::separator() + fi.filePath; if (fi.isSymLink) { QString destination = QFile::decodeName(fileData(fi.filePath)); @@ -954,7 +1046,7 @@ bool MQZipReader::extractAll(const QString &destinationDir) const } } - foreach (FileInfo fi, allFiles) { + for (const FileInfo &fi : allFiles) { const QString absPath = destinationDir + QDir::separator() + fi.filePath; if (fi.isFile) { QFile f(absPath); @@ -1021,9 +1113,8 @@ void MQZipReader::close() MQZipWriter::MQZipWriter(const QString &fileName, QIODevice::OpenMode mode) { QScopedPointer f(new QFile(fileName)); - f->open(mode); MQZipWriter::Status status; - if (f->error() == QFile::NoError) + if (f->open(mode) && f->error() == QFile::NoError) status = MQZipWriter::NoError; else { if (f->error() == QFile::WriteError) @@ -1067,7 +1158,7 @@ QIODevice* MQZipWriter::device() const } /*! - Returns true if the user can write to the archive; otherwise returns false. + Returns \c true if the user can write to the archive; otherwise returns \c false. */ bool MQZipWriter::isWritable() const { @@ -1075,7 +1166,7 @@ bool MQZipWriter::isWritable() const } /*! - Returns true if the file exists; otherwise returns false. + Returns \c true if the file exists; otherwise returns \c false. */ bool MQZipWriter::exists() const { @@ -1175,7 +1266,7 @@ QFile::Permissions MQZipWriter::creationPermissions() const */ void MQZipWriter::addFile(const QString &fileName, const QByteArray &data) { - d->addEntry(MQZipWriterPrivate::File, fileName, data); + d->addEntry(MQZipWriterPrivate::File, QDir::fromNativeSeparators(fileName), data); } /*! @@ -1197,7 +1288,7 @@ void MQZipWriter::addFile(const QString &fileName, QIODevice *device) return; } } - d->addEntry(MQZipWriterPrivate::File, fileName, device->readAll()); + d->addEntry(MQZipWriterPrivate::File, QDir::fromNativeSeparators(fileName), device->readAll()); if (opened) device->close(); } @@ -1208,10 +1299,10 @@ void MQZipWriter::addFile(const QString &fileName, QIODevice *device) */ void MQZipWriter::addDirectory(const QString &dirName) { - QString name = dirName; + QString name(QDir::fromNativeSeparators(dirName)); // separator is mandatory - if (!name.endsWith(QDir::separator())) - name.append(QDir::separator()); + if (!name.endsWith(QLatin1Char('/'))) + name.append(QLatin1Char('/')); d->addEntry(MQZipWriterPrivate::Directory, name, QByteArray()); } @@ -1222,7 +1313,7 @@ void MQZipWriter::addDirectory(const QString &dirName) */ void MQZipWriter::addSymLink(const QString &fileName, const QString &destination) { - d->addEntry(MQZipWriterPrivate::Symlink, fileName, QFile::encodeName(destination)); + d->addEntry(MQZipWriterPrivate::Symlink, QDir::fromNativeSeparators(fileName), QFile::encodeName(destination)); } /*! @@ -1240,7 +1331,7 @@ void MQZipWriter::close() // write new directory for (int i = 0; i < d->fileHeaders.size(); ++i) { const FileHeader &header = d->fileHeaders.at(i); - d->device->write((const char *)&header.h, sizeof(MCentralFileHeader)); + d->device->write((const char *)&header.h, sizeof(CentralFileHeader)); d->device->write(header.file_name); d->device->write(header.extra_field); d->device->write(header.file_comment); diff --git a/thirdparty/qzip/qzipreader_p.h b/thirdparty/qzip/qzipreader_p.h index dbf971dad8..a3d7354a9a 100644 --- a/thirdparty/qzip/qzipreader_p.h +++ b/thirdparty/qzip/qzipreader_p.h @@ -78,23 +78,23 @@ public: struct FileInfo { - FileInfo(); - FileInfo(const FileInfo &other); - ~FileInfo(); - FileInfo &operator=(const FileInfo &other); - bool isValid() const; + FileInfo() Q_DECL_NOTHROW + : isDir(false), isFile(false), isSymLink(false), crc(0), size(0) + {} + + bool isValid() const Q_DECL_NOTHROW { return isDir || isFile || isSymLink; } + QString filePath; uint isDir : 1; uint isFile : 1; uint isSymLink : 1; QFile::Permissions permissions; - uint crc32; + uint crc; qint64 size; QDateTime lastModified; - void *d; }; - QList fileInfoList() const; + QVector fileInfoList() const; int count() const; FileInfo entryInfoAt(int index) const; diff --git a/zerberus/instrument.cpp b/zerberus/instrument.cpp index 5baeffefaa..22eb538731 100644 --- a/zerberus/instrument.cpp +++ b/zerberus/instrument.cpp @@ -43,7 +43,7 @@ Sample::~Sample() Sample* ZInstrument::readSample(const QString& s, MQZipReader* uz) { if (uz) { - QList fi = uz->fileInfoList(); + QVector fi = uz->fileInfoList(); buf = uz->fileData(s); if (buf.isEmpty()) { diff --git a/zerberus/sfz.cpp b/zerberus/sfz.cpp index c1837029b7..8e6f627e0a 100644 --- a/zerberus/sfz.cpp +++ b/zerberus/sfz.cpp @@ -446,8 +446,8 @@ void SfzRegion::readOp(const QString& b, const QString& data, SfzControl &c) loop_mode = LoopMode::CONTINUOUS; else if (opcode_data == "loop_sustain") loop_mode = LoopMode::SUSTAIN; - if (loop_mode != LoopMode::ONE_SHOT) - qDebug("SfzRegion: loop_mode <%s>", qPrintable(opcode_data)); + //if (loop_mode != LoopMode::ONE_SHOT) + // qDebug("SfzRegion: loop_mode <%s>", qPrintable(opcode_data)); } else if(opcode == "loop_start") readLongLong(opcode_data, loopStart); diff --git a/zerberus/zerberus.cpp b/zerberus/zerberus.cpp index 46f474caac..b85d82b873 100644 --- a/zerberus/zerberus.cpp +++ b/zerberus/zerberus.cpp @@ -47,8 +47,8 @@ Zerberus::Zerberus() initialized = true; Voice::init(); } - for (int i = 0; i < MAX_VOICES; ++i) - freeVoices.push(new Voice(this)); + + freeVoices.init(this); for (int i = 0; i < MAX_CHANNEL; ++i) _channel[i] = new Channel(this, i); busy = true; // no sf loaded yet diff --git a/zerberus/zerberus.h b/zerberus/zerberus.h index 2be8257362..f493033977 100644 --- a/zerberus/zerberus.h +++ b/zerberus/zerberus.h @@ -17,11 +17,14 @@ #include // #include #include +#include +#include #include "synthesizer/synthesizer.h" #include "synthesizer/event.h" #include "voice.h" + class Channel; class ZInstrument; enum class Trigger : char; @@ -35,32 +38,33 @@ static const int MAX_TRIGGER = 512; //--------------------------------------------------------- class VoiceFifo { - Voice* buffer[MAX_VOICES]; - std::atomic n; - int writeIdx = 0; // index of next slot to write - int readIdx = 0; // index of slot to read + std::queue buffer; + std::vector< std::unique_ptr > voices; public: VoiceFifo() { - n = 0; + voices.resize(MAX_VOICES); } - ~VoiceFifo() { - for (Voice* v : buffer) - delete v; + + void init(Zerberus* z) { + for (int i = 0; i < MAX_VOICES; ++i) { + voices.push_back(std::unique_ptr(new Voice(z))); + buffer.push(voices.back().get()); + } } + void push(Voice* v) { - buffer[writeIdx++] = v; - writeIdx %= MAX_VOICES; - ++n; + buffer.push(v); } - Voice* pop() { - Q_ASSERT(n != 0); - --n; - Voice* v = buffer[readIdx++]; - readIdx %= MAX_VOICES; + + Voice* pop() { + Q_ASSERT(!buffer.empty()); + Voice* v = buffer.front(); + buffer.pop(); return v; } - bool empty() const { return n == 0; } + + bool empty() const { return buffer.empty(); } }; //--------------------------------------------------------- diff --git a/zerberus/zerberusgui.cpp b/zerberus/zerberusgui.cpp index f8e64746c8..42d0ac694f 100644 --- a/zerberus/zerberusgui.cpp +++ b/zerberus/zerberusgui.cpp @@ -13,6 +13,7 @@ #include "zerberusgui.h" #include "mscore/preferences.h" +#include "mscore/extension.h" //--------------------------------------------------------- // SfzListDialog @@ -137,6 +138,10 @@ QFileInfoList Zerberus::sfzFiles() QStringList pl = Ms::preferences.getString(PREF_APP_PATHS_MYSOUNDFONTS).split(";"); pl.prepend(QFileInfo(QString("%1%2").arg(Ms::mscoreGlobalShare).arg("sound")).absoluteFilePath()); + // append extensions directory + QStringList extensionsDir = Ms::Extension::getDirectoriesByType(Ms::Extension::sfzsDir); + pl.append(extensionsDir); + foreach (const QString& s, pl) { QString ss(s); if (!s.isEmpty() && s[0] == '~')