Browse Source

Introjucer update to provide downloading of new modules directly from the website.

tags/2021-05-28
Julian Storer 13 years ago
parent
commit
2d56bedab5
18 changed files with 606 additions and 400 deletions
  1. +49
    -6
      extras/Introjucer/Source/Application/jucer_Application.h
  2. +1
    -1
      extras/Introjucer/Source/Application/jucer_CommandLine.cpp
  3. +1
    -1
      extras/Introjucer/Source/Application/jucer_CommonHeaders.h
  4. +299
    -252
      extras/Introjucer/Source/Application/jucer_JuceUpdater.cpp
  5. +33
    -23
      extras/Introjucer/Source/Application/jucer_JuceUpdater.h
  6. +1
    -1
      extras/Introjucer/Source/Application/jucer_MainWindow.cpp
  7. +11
    -12
      extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp
  8. +1
    -1
      extras/Introjucer/Source/Project Saving/jucer_ProjectSaver.h
  9. +129
    -20
      extras/Introjucer/Source/Project/jucer_Module.cpp
  10. +22
    -2
      extras/Introjucer/Source/Project/jucer_Module.h
  11. +2
    -1
      extras/Introjucer/Source/Project/jucer_NewProjectWizard.cpp
  12. +0
    -12
      extras/Introjucer/Source/Project/jucer_Project.cpp
  13. +46
    -31
      extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp
  14. +8
    -15
      extras/Introjucer/Source/Utility/jucer_FileHelpers.cpp
  15. +2
    -1
      extras/Introjucer/Source/Utility/jucer_FileHelpers.h
  16. +0
    -17
      extras/Introjucer/Source/Utility/jucer_StoredSettings.cpp
  17. +0
    -3
      extras/Introjucer/Source/Utility/jucer_StoredSettings.h
  18. +1
    -1
      modules/juce_core/system/juce_StandardHeader.h

+ 49
- 6
extras/Introjucer/Source/Application/jucer_Application.h View File

@@ -38,7 +38,10 @@ class JucerApplication : public JUCEApplication
{
public:
//==============================================================================
JucerApplication() {}
JucerApplication()
{
}
~JucerApplication() {}
//==============================================================================
@@ -96,12 +99,18 @@ public:
mainWindows.clear();
OpenDocumentManager::deleteInstance();
deleteAndZero (commandManager);
commandManager = nullptr;
}
//==============================================================================
void systemRequestedQuit()
{
if (cancelAnyModalComponents())
{
new AsyncQuitRetrier();
return;
}
while (mainWindows.size() > 0)
{
if (! mainWindows[0]->closeCurrentProject())
@@ -139,11 +148,11 @@ public:
bool moreThanOneInstanceAllowed()
{
#ifndef JUCE_LINUX
#ifndef JUCE_LINUX
return false;
#else
#else
return true; //xxx should be false but doesn't work on linux..
#endif
#endif
}
void anotherInstanceStarted (const String& commandLine)
@@ -352,7 +361,12 @@ public:
case CommandIDs::showPrefs: showPrefsPanel(); break;
case CommandIDs::saveAll: OpenDocumentManager::getInstance()->saveAll(); break;
case CommandIDs::closeAllDocuments: closeAllDocuments (true); break;
case CommandIDs::showJuceVersion: JuceUpdater::show (mainWindows[0]); break;
case CommandIDs::showJuceVersion:
{
ModuleList list (ModuleList::getDefaultModulesFolder (nullptr));
JuceUpdater::show (list, mainWindows[0]);
break;
}
default: return JUCEApplication::perform (info);
}
@@ -503,6 +517,35 @@ private:
return createNewMainWindow();
}
//==============================================================================
static bool cancelAnyModalComponents()
{
const int numModal = ModalComponentManager::getInstance()->getNumModalComponents();
for (int i = numModal; --i >= 0;)
if (ModalComponentManager::getInstance()->getModalComponent(i) != nullptr)
ModalComponentManager::getInstance()->getModalComponent(i)->exitModalState (0);
return numModal > 0;
}
class AsyncQuitRetrier : public Timer
{
public:
AsyncQuitRetrier() { startTimer (500); }
void timerCallback()
{
stopTimer();
delete this;
if (JUCEApplication::getInstance() != nullptr)
JUCEApplication::getInstance()->systemRequestedQuit();
}
JUCE_DECLARE_NON_COPYABLE (AsyncQuitRetrier);
};
};


+ 1
- 1
extras/Introjucer/Source/Application/jucer_CommandLine.cpp View File

@@ -192,7 +192,7 @@ namespace
int listModules()
{
std::cout << "Downloading list of available modules..." << std::endl;
ModuleList list;
ModuleList list (File::nonexistent);
list.loadFromWebsite();
for (int i = 0; i < list.modules.size(); ++i)


+ 1
- 1
extras/Introjucer/Source/Application/jucer_CommonHeaders.h View File

@@ -37,7 +37,7 @@
#include "jucer_CommandIDs.h"
//==============================================================================
extern ApplicationCommandManager* commandManager;
extern ScopedPointer<ApplicationCommandManager> commandManager;
//==============================================================================
const char* const projectItemDragType = "Project Items";


+ 299
- 252
extras/Introjucer/Source/Application/jucer_JuceUpdater.cpp View File

@@ -25,30 +25,45 @@
#include "../jucer_Headers.h"
#include "jucer_JuceUpdater.h"
#include "../Project/jucer_Module.h"
//==============================================================================
JuceUpdater::JuceUpdater()
: filenameComp ("Juce Folder", StoredSettings::getInstance()->getLastKnownJuceFolder(),
JuceUpdater::JuceUpdater (ModuleList& moduleList_)
: moduleList (moduleList_),
latestList (File::nonexistent),
filenameComp ("Juce Folder", ModuleList::getLocalModulesFolder (nullptr),
true, true, false, "*", String::empty, "Select your Juce folder"),
checkNowButton ("Check Online for Available Updates...",
"Contacts the website to see if this version is up-to-date")
checkNowButton ("Check for available updates on the JUCE website...",
"Contacts the website to see if new modules are available"),
installButton ("Download and install selected modules..."),
selectAllButton ("Select/Deselect All")
{
addAndMakeVisible (&label);
addAndMakeVisible (&currentVersionLabel);
addAndMakeVisible (&filenameComp);
addAndMakeVisible (&checkNowButton);
addAndMakeVisible (&currentVersionLabel);
addAndMakeVisible (&installButton);
addAndMakeVisible (&selectAllButton);
checkNowButton.addListener (this);
installButton.addListener (this);
selectAllButton.addListener (this);
filenameComp.addListener (this);
currentVersionLabel.setFont (Font (14.0f, Font::italic));
label.setFont (Font (12.0f));
label.setText ("Destination folder:", false);
label.setText ("Local modules folder:", false);
addAndMakeVisible (&availableVersionsList);
availableVersionsList.setModel (this);
setSize (600, 300);
updateInstallButtonStatus();
versionsToDownload = ValueTree ("modules");
versionsToDownload.addListener (this);
setSize (600, 500);
checkNow();
}
JuceUpdater::~JuceUpdater()
@@ -57,12 +72,32 @@ JuceUpdater::~JuceUpdater()
filenameComp.removeListener (this);
}
void JuceUpdater::show (Component* mainWindow)
//==============================================================================
class UpdateDialogWindow : public DialogWindow
{
JuceUpdater updater;
DialogWindow::showModalDialog ("Juce Update...", &updater, mainWindow,
Colours::lightgrey,
true, false, false);
public:
UpdateDialogWindow (JuceUpdater* updater, Component* componentToCentreAround)
: DialogWindow ("JUCE Module Updater",
Colours::lightgrey, true, true)
{
setContentOwned (updater, true);
centreAroundComponent (componentToCentreAround, getWidth(), getHeight());
setResizable (true, true);
}
void closeButtonPressed()
{
setVisible (false);
}
private:
JUCE_DECLARE_NON_COPYABLE (UpdateDialogWindow);
};
void JuceUpdater::show (ModuleList& moduleList, Component* mainWindow)
{
UpdateDialogWindow w (new JuceUpdater (moduleList), mainWindow);
w.runModalLoop();
}
void JuceUpdater::resized()
@@ -70,9 +105,16 @@ void JuceUpdater::resized()
filenameComp.setBounds (20, 40, getWidth() - 40, 22);
label.setBounds (filenameComp.getX(), filenameComp.getY() - 18, filenameComp.getWidth(), 18);
currentVersionLabel.setBounds (filenameComp.getX(), filenameComp.getBottom(), filenameComp.getWidth(), 25);
checkNowButton.changeWidthToFitText (20);
checkNowButton.setCentrePosition (getWidth() / 2, filenameComp.getBottom() + 40);
availableVersionsList.setBounds (filenameComp.getX(), checkNowButton.getBottom() + 20, filenameComp.getWidth(), getHeight() - (checkNowButton.getBottom() + 20));
checkNowButton.changeWidthToFitText (22);
checkNowButton.setCentrePosition (getWidth() / 2, filenameComp.getBottom() + 20);
availableVersionsList.setBounds (filenameComp.getX(), checkNowButton.getBottom() + 20,
filenameComp.getWidth(),
getHeight() - 30 - (checkNowButton.getBottom() + 20));
installButton.changeWidthToFitText (22);
installButton.setTopRightPosition (availableVersionsList.getRight(), getHeight() - 28);
selectAllButton.setBounds (availableVersionsList.getX(),
availableVersionsList.getBottom() + 4,
installButton.getX() - availableVersionsList.getX() - 20, 22);
}
void JuceUpdater::paint (Graphics& g)
@@ -80,325 +122,330 @@ void JuceUpdater::paint (Graphics& g)
g.fillAll (Colours::white);
}
String findVersionNum (const String& file, const String& token)
void JuceUpdater::buttonClicked (Button* b)
{
return file.fromFirstOccurrenceOf (token, false, false)
.upToFirstOccurrenceOf ("\n", false, false)
.trim();
if (b == &installButton)
install();
else if (b == &selectAllButton)
selectAll();
else
checkNow();
}
String JuceUpdater::getCurrentVersion()
void JuceUpdater::refresh()
{
const String header (filenameComp.getCurrentFile()
.getChildFile ("src/core/juce_StandardHeader.h").loadFileAsString());
availableVersionsList.updateContent();
availableVersionsList.repaint();
}
const String v1 (findVersionNum (header, "JUCE_MAJOR_VERSION"));
const String v2 (findVersionNum (header, "JUCE_MINOR_VERSION"));
const String v3 (findVersionNum (header, "JUCE_BUILDNUMBER"));
class WebsiteContacterThread : public Thread,
private AsyncUpdater
{
public:
WebsiteContacterThread (JuceUpdater& owner_, const ModuleList& latestList)
: Thread ("Module updater"),
owner (owner_),
downloaded (latestList)
{
startThread();
}
if ((v1 + v2 + v3).isEmpty())
return String::empty;
~WebsiteContacterThread()
{
stopThread (10000);
}
void run()
{
if (downloaded.loadFromWebsite())
triggerAsyncUpdate();
else
AlertWindow::showMessageBox (AlertWindow::InfoIcon,
"Module Update",
"Couldn't connect to the website!");
}
void handleAsyncUpdate()
{
owner.backgroundUpdateComplete (downloaded);
}
private:
JuceUpdater& owner;
ModuleList downloaded;
};
return v1 + "." + v2 + "." + v3;
void JuceUpdater::checkNow()
{
websiteContacterThread = nullptr;
websiteContacterThread = new WebsiteContacterThread (*this, latestList);
}
XmlElement* JuceUpdater::downloadVersionList()
void JuceUpdater::backgroundUpdateComplete (const ModuleList& newList)
{
return URL ("http://www.rawmaterialsoftware.com/juce/downloads/juce_versions.php").readEntireXmlStream();
latestList = newList;
websiteContacterThread = nullptr;
if (latestList == moduleList)
AlertWindow::showMessageBox (AlertWindow::InfoIcon,
"Module Update",
"No new modules are available");
refresh();
}
void JuceUpdater::updateVersions (const XmlElement& xml)
int JuceUpdater::getNumCheckedModules() const
{
availableVersions.clear();
int numChecked = 0;
forEachXmlChildElementWithTagName (xml, v, "VERSION")
{
VersionInfo* vi = new VersionInfo();
vi->url = URL (v->getStringAttribute ("url"));
vi->desc = v->getStringAttribute ("desc");
vi->version = v->getStringAttribute ("version");
vi->date = v->getStringAttribute ("date");
availableVersions.add (vi);
}
for (int i = latestList.modules.size(); --i >= 0;)
if (versionsToDownload [latestList.modules.getUnchecked(i)->uid])
++numChecked;
availableVersionsList.updateContent();
return numChecked;
}
void JuceUpdater::buttonClicked (Button*)
bool JuceUpdater::isLatestVersion (const String& moduleID) const
{
ScopedPointer<XmlElement> xml (downloadVersionList());
const ModuleList::Module* m1 = moduleList.findModuleInfo (moduleID);
const ModuleList::Module* m2 = latestList.findModuleInfo (moduleID);
if (xml == nullptr || xml->hasTagName ("html"))
{
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "Connection Problems...",
"Couldn't connect to the Raw Material Software website!");
return m1 != nullptr && m2 != nullptr && m1->version == m2->version;
}
return;
}
void JuceUpdater::updateInstallButtonStatus()
{
const int numChecked = getNumCheckedModules();
installButton.setEnabled (numChecked > 0);
selectAllButton.setToggleState (numChecked > latestList.modules.size() / 2, false);
}
if (! xml->hasTagName ("JUCEVERSIONS"))
{
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "Update Problems...",
"This version of the Introjucer may be too old to receive automatic updates!\n\n"
"Please visit www.rawmaterialsoftware.com and get the latest version manually!");
return;
}
void JuceUpdater::filenameComponentChanged (FilenameComponent*)
{
moduleList.rescan (filenameComp.getCurrentFile());
filenameComp.setCurrentFile (moduleList.getModulesFolder(), true, false);
const String currentVersion (getCurrentVersion());
if (! FileHelpers::isModulesFolder (moduleList.getModulesFolder()))
currentVersionLabel.setText ("(Not a Juce folder)", false);
else
currentVersionLabel.setText (String::empty, false);
OwnedArray<VersionInfo> versions;
updateVersions (*xml);
refresh();
}
//==============================================================================
class NewVersionDownloader : public ThreadWithProgressWindow
void JuceUpdater::selectAll()
{
public:
NewVersionDownloader (const String& title, const URL& url_, const File& target_)
: ThreadWithProgressWindow (title, true, true),
url (url_), target (target_)
bool enable = getNumCheckedModules() < latestList.modules.size() / 2;
versionsToDownload.removeAllProperties (nullptr);
if (enable)
{
for (int i = latestList.modules.size(); --i >= 0;)
if (! isLatestVersion (latestList.modules.getUnchecked(i)->uid))
versionsToDownload.setProperty (latestList.modules.getUnchecked(i)->uid, true, nullptr);
}
}
void run()
{
setStatusMessage ("Contacting website...");
//==============================================================================
int JuceUpdater::getNumRows()
{
return latestList.modules.size();
}
ScopedPointer<InputStream> input (url.createInputStream (false));
void JuceUpdater::paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected)
{
if (rowIsSelected)
g.fillAll (findColour (TextEditor::highlightColourId));
}
if (input == nullptr)
Component* JuceUpdater::refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate)
{
class UpdateListComponent : public Component
{
public:
UpdateListComponent (JuceUpdater& updater_)
: updater (updater_)
{
error = "Couldn't connect to the website...";
return;
addChildComponent (&toggle);
toggle.setBounds ("2, 2, parent.height - 2, parent.height - 2");
toggle.setWantsKeyboardFocus (false);
setInterceptsMouseClicks (false, true);
}
if (! target.deleteFile())
void setModule (const ModuleList::Module* newModule,
const ModuleList::Module* existingModule,
const Value& value)
{
error = "Couldn't delete the destination file...";
return;
if (newModule != nullptr)
{
toggle.getToggleStateValue().referTo (value);
toggle.setVisible (true);
toggle.setEnabled (true);
name = newModule->uid;
status = String::empty;
if (existingModule == nullptr)
{
status << " (not currently installed)";
}
else if (existingModule->version != newModule->version)
{
status << " installed: " << existingModule->version
<< ", available: " << newModule->version;
}
else
{
status << " (latest version already installed)";
toggle.setEnabled (false);
}
}
else
{
name = status = String::empty;
toggle.setVisible (false);
}
}
ScopedPointer<OutputStream> output (target.createOutputStream (32768));
if (output == nullptr)
void paint (Graphics& g)
{
error = "Couldn't write to the destination file...";
return;
}
g.setColour (Colours::green.withAlpha (0.12f));
setStatusMessage ("Downloading...");
g.fillRect (0, 1, getWidth(), getHeight() - 2);
g.setColour (Colours::black);
g.setFont (getHeight() * 0.7f);
int totalBytes = (int) input->getTotalLength();
int bytesSoFar = 0;
g.drawText (name, toggle.getRight() + 4, 0, getWidth() / 2 - toggle.getRight() - 4, getHeight(),
Justification::centredLeft, true);
while (! (input->isExhausted() || threadShouldExit()))
{
HeapBlock<char> buffer (8192);
const int num = input->read (buffer, 8192);
g.drawText (status, getWidth() / 2, 0, getWidth() / 2, getHeight(),
Justification::centredLeft, true);
}
if (num == 0)
break;
private:
JuceUpdater& updater;
ToggleButton toggle;
String name, status;
};
output->write (buffer, num);
bytesSoFar += num;
UpdateListComponent* c = dynamic_cast <UpdateListComponent*> (existingComponentToUpdate);
if (c == nullptr)
c = new UpdateListComponent (*this);
setProgress (totalBytes > 0 ? bytesSoFar / (double) totalBytes : -1.0);
}
}
ModuleList::Module* m = latestList.modules [rowNumber];
String error;
if (m != nullptr)
c->setModule (m,
moduleList.findModuleInfo (m->uid),
versionsToDownload.getPropertyAsValue (m->uid, nullptr));
else
c->setModule (nullptr, nullptr, Value());
private:
URL url;
File target;
};
return c;
}
//==============================================================================
class Unzipper : public ThreadWithProgressWindow
class InstallThread : public ThreadWithProgressWindow
{
public:
Unzipper (ZipFile& zipFile_, const File& targetDir_)
: ThreadWithProgressWindow ("Unzipping...", true, true),
worked (true), zipFile (zipFile_), targetDir (targetDir_)
InstallThread (const ModuleList& targetList_,
const ModuleList& list_, const StringArray& itemsToInstall_)
: ThreadWithProgressWindow ("Installing New Modules", true, true),
result (Result::ok()),
targetList (targetList_),
list (list_),
itemsToInstall (itemsToInstall_)
{
}
void run()
{
for (int i = 0; i < zipFile.getNumEntries(); ++i)
for (int i = 0; i < itemsToInstall.size(); ++i)
{
if (threadShouldExit())
break;
const ModuleList::Module* m = list.findModuleInfo (itemsToInstall[i]);
const ZipFile::ZipEntry* e = zipFile.getEntry (i);
setStatusMessage ("Unzipping " + e->filename + "...");
setProgress (i / (double) zipFile.getNumEntries());
worked = zipFile.uncompressEntry (i, targetDir, true) && worked;
}
}
jassert (m != nullptr);
if (m != nullptr)
{
setProgress (i / (double) itemsToInstall.size());
bool worked;
MemoryBlock downloaded;
result = download (*m, downloaded);
private:
ZipFile& zipFile;
File targetDir;
};
if (result.failed())
break;
//==============================================================================
void JuceUpdater::applyVersion (VersionInfo* version)
{
File destDir (filenameComp.getCurrentFile());
if (threadShouldExit())
break;
const bool destDirExisted = destDir.isDirectory();
result = unzip (*m, downloaded);
if (destDirExisted && destDir.getNumberOfChildFiles (File::findFilesAndDirectories, "*") > 0)
{
int r = AlertWindow::showYesNoCancelBox (AlertWindow::WarningIcon, "Folder already exists",
"The folder " + destDir.getFullPathName() + "\nalready contains some files...\n\n"
"Do you want to delete everything in the folder and replace it entirely, or just merge the new files into the existing folder?",
"Delete and replace entire folder",
"Add and overwrite existing files",
"Cancel");
if (r == 0)
return;
if (r == 1)
{
if (! destDir.deleteRecursively())
{
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "Problems...",
"Couldn't delete the existing folder!");
return;
if (result.failed())
break;
}
if (threadShouldExit())
break;
}
}
if (! (destDir.isDirectory() || destDir.createDirectory()))
Result download (const ModuleList::Module& m, MemoryBlock& dest)
{
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "Problems...",
"Couldn't create that target folder..");
return;
}
File zipFile (destDir.getNonexistentChildFile ("juce_download", ".tar.gz", false));
setStatusMessage ("Downloading " + m.uid + "...");
bool worked = false;
if (m.url.readEntireBinaryStream (dest, false))
return Result::ok();
{
NewVersionDownloader downloader ("Downloading Version " + version->version + "...",
version->url, zipFile);
worked = downloader.runThread();
return Result::fail ("Failed to download from: " + m.url.toString (false));
}
if (worked)
Result unzip (const ModuleList::Module& m, const MemoryBlock& data)
{
ZipFile zip (zipFile);
Unzipper unzipper (zip, destDir);
worked = unzipper.runThread() && unzipper.worked;
}
zipFile.deleteFile();
setStatusMessage ("Installing " + m.uid + "...");
if ((! destDirExisted) && (destDir.getNumberOfChildFiles (File::findFilesAndDirectories, "*") == 0 || ! worked))
destDir.deleteRecursively();
MemoryInputStream input (data, false);
ZipFile zip (input);
filenameComponentChanged (&filenameComp);
}
void JuceUpdater::filenameComponentChanged (FilenameComponent*)
{
const String version (getCurrentVersion());
if (zip.getNumEntries() == 0)
return Result::fail ("The downloaded file wasn't a valid module file!");
if (version.isEmpty())
currentVersionLabel.setText ("(Not a Juce folder)", false);
else
currentVersionLabel.setText ("(Current version in this folder: " + version + ")", false);
}
return zip.uncompressTo (targetList.getModulesFolder(), true);
}
//==============================================================================
int JuceUpdater::getNumRows()
{
return availableVersions.size();
}
Result result;
void JuceUpdater::paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected)
{
if (rowIsSelected)
g.fillAll (findColour (TextEditor::highlightColourId));
}
private:
ModuleList targetList, list;
StringArray itemsToInstall;
};
Component* JuceUpdater::refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate)
void JuceUpdater::install()
{
class UpdateListComponent : public Component,
public ButtonListener
if (! moduleList.getModulesFolder().createDirectory())
{
public:
UpdateListComponent (JuceUpdater& updater_)
: updater (updater_),
version (nullptr),
applyButton ("Install this version...")
{
addAndMakeVisible (&applyButton);
applyButton.addListener (this);
setInterceptsMouseClicks (false, true);
}
~UpdateListComponent()
{
applyButton.removeListener (this);
}
void setVersion (VersionInfo* v)
{
if (version != v)
{
version = v;
repaint();
resized();
}
}
void paint (Graphics& g)
{
if (version != nullptr)
{
g.setColour (Colours::green.withAlpha (0.12f));
g.fillRect (0, 1, getWidth(), getHeight() - 2);
g.setColour (Colours::black);
g.setFont (getHeight() * 0.7f);
String s;
s << "Version " << version->version << " - " << version->desc << " - " << version->date;
g.drawText (s, 4, 0, applyButton.getX() - 4, getHeight(), Justification::centredLeft, true);
}
}
void resized()
{
applyButton.changeWidthToFitText (getHeight() - 4);
applyButton.setTopRightPosition (getWidth(), 2);
applyButton.setVisible (version != nullptr);
}
AlertWindow::showMessageBox (AlertWindow::WarningIcon,
"Module Update",
"Couldn't create the target folder!");
return;
}
void buttonClicked (Button*)
{
updater.applyVersion (version);
}
StringArray itemsWanted;
private:
JuceUpdater& updater;
VersionInfo* version;
TextButton applyButton;
};
for (int i = latestList.modules.size(); --i >= 0;)
if (versionsToDownload [latestList.modules.getUnchecked(i)->uid])
itemsWanted.add (latestList.modules.getUnchecked(i)->uid);
UpdateListComponent* c = dynamic_cast <UpdateListComponent*> (existingComponentToUpdate);
if (c == nullptr)
c = new UpdateListComponent (*this);
{
InstallThread thread (moduleList, latestList, itemsWanted);
thread.runThread();
}
c->setVersion (availableVersions [rowNumber]);
return c;
moduleList.rescan();
refresh();
}
void JuceUpdater::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateInstallButtonStatus(); }
void JuceUpdater::valueTreeChildAdded (ValueTree&, ValueTree&) {}
void JuceUpdater::valueTreeChildRemoved (ValueTree&, ValueTree&) {}
void JuceUpdater::valueTreeChildOrderChanged (ValueTree&) {}
void JuceUpdater::valueTreeParentChanged (ValueTree&) {}

+ 33
- 23
extras/Introjucer/Source/Application/jucer_JuceUpdater.h View File

@@ -26,18 +26,21 @@
#ifndef __JUCER_JUCEUPDATER_JUCEHEADER__
#define __JUCER_JUCEUPDATER_JUCEHEADER__
#include "../Project/jucer_Module.h"
//==============================================================================
class JuceUpdater : public Component,
public ButtonListener,
public FilenameComponentListener,
public ListBoxModel
private ButtonListener,
private FilenameComponentListener,
private ListBoxModel,
private ValueTree::Listener
{
public:
JuceUpdater();
JuceUpdater (ModuleList& moduleList);
~JuceUpdater();
static void show (Component* mainWindow);
static void show (ModuleList& moduleList, Component* mainWindow);
//==============================================================================
void resized();
@@ -49,29 +52,36 @@ public:
void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected);
Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate);
void backgroundUpdateComplete (const ModuleList& newList);
private:
ModuleList& moduleList;
ModuleList latestList;
Label label, currentVersionLabel;
FilenameComponent filenameComp;
TextButton checkNowButton;
ListBox availableVersionsList;
XmlElement* downloadVersionList();
String getCurrentVersion();
bool isAlreadyUpToDate();
struct VersionInfo
{
URL url;
String desc;
String version;
String date;
};
OwnedArray<VersionInfo> availableVersions;
void updateVersions (const XmlElement& xml);
void applyVersion (VersionInfo* version);
ValueTree versionsToDownload;
TextButton installButton;
ToggleButton selectAllButton;
ScopedPointer<Thread> websiteContacterThread;
void checkNow();
void install();
void updateInstallButtonStatus();
void refresh();
void selectAll();
int getNumCheckedModules() const;
bool isLatestVersion (const String& moduleID) const;
void valueTreePropertyChanged (ValueTree&, const Identifier&);
void valueTreeChildAdded (ValueTree&, ValueTree&);
void valueTreeChildRemoved (ValueTree&, ValueTree&);
void valueTreeChildOrderChanged (ValueTree&);
void valueTreeParentChanged (ValueTree&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceUpdater);
};
#endif // __JUCER_JUCEUPDATER_JUCEHEADER__

+ 1
- 1
extras/Introjucer/Source/Application/jucer_MainWindow.cpp View File

@@ -30,7 +30,7 @@
#include "../Code Editor/jucer_SourceCodeEditor.h"
#include "../Project/jucer_NewProjectWizard.h"
ApplicationCommandManager* commandManager = nullptr;
ScopedPointer<ApplicationCommandManager> commandManager;
//==============================================================================


+ 11
- 12
extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp View File

@@ -66,18 +66,18 @@ ProjectExporter* ProjectExporter::createNewExporter (Project& project, const int
switch (index)
{
case 0: exp = new XCodeProjectExporter (project, ValueTree (XCodeProjectExporter::getValueTreeTypeName (false)), false); break;
case 1: exp = new XCodeProjectExporter (project, ValueTree (XCodeProjectExporter::getValueTreeTypeName (true)), true); break;
case 2: exp = new MSVCProjectExporterVC6 (project, ValueTree (MSVCProjectExporterVC6::getValueTreeTypeName())); break;
case 0: exp = new XCodeProjectExporter (project, ValueTree (XCodeProjectExporter ::getValueTreeTypeName (false)), false); break;
case 1: exp = new XCodeProjectExporter (project, ValueTree (XCodeProjectExporter ::getValueTreeTypeName (true)), true); break;
case 2: exp = new MSVCProjectExporterVC6 (project, ValueTree (MSVCProjectExporterVC6 ::getValueTreeTypeName())); break;
case 3: exp = new MSVCProjectExporterVC2005 (project, ValueTree (MSVCProjectExporterVC2005::getValueTreeTypeName())); break;
case 4: exp = new MSVCProjectExporterVC2008 (project, ValueTree (MSVCProjectExporterVC2008::getValueTreeTypeName())); break;
case 5: exp = new MSVCProjectExporterVC2010 (project, ValueTree (MSVCProjectExporterVC2010::getValueTreeTypeName())); break;
case 6: exp = new MakefileProjectExporter (project, ValueTree (MakefileProjectExporter::getValueTreeTypeName())); break;
case 7: exp = new AndroidProjectExporter (project, ValueTree (AndroidProjectExporter::getValueTreeTypeName())); break;
case 6: exp = new MakefileProjectExporter (project, ValueTree (MakefileProjectExporter ::getValueTreeTypeName())); break;
case 7: exp = new AndroidProjectExporter (project, ValueTree (AndroidProjectExporter ::getValueTreeTypeName())); break;
default: jassertfalse; return 0;
}
File juceFolder (StoredSettings::getInstance()->getLastKnownJuceFolder());
File juceFolder (ModuleList::getLocalModulesFolder (&project));
File target (exp->getTargetFolder());
if (FileHelpers::shouldPathsBeRelative (juceFolder.getFullPathName(), project.getFile().getFullPathName()))
@@ -95,13 +95,13 @@ ProjectExporter* ProjectExporter::createNewExporter (Project& project, const Str
ProjectExporter* ProjectExporter::createExporter (Project& project, const ValueTree& settings)
{
ProjectExporter* exp = MSVCProjectExporterVC6::createForSettings (project, settings);
ProjectExporter* exp = MSVCProjectExporterVC6 ::createForSettings (project, settings);
if (exp == nullptr) exp = MSVCProjectExporterVC2005::createForSettings (project, settings);
if (exp == nullptr) exp = MSVCProjectExporterVC2008::createForSettings (project, settings);
if (exp == nullptr) exp = MSVCProjectExporterVC2010::createForSettings (project, settings);
if (exp == nullptr) exp = XCodeProjectExporter::createForSettings (project, settings);
if (exp == nullptr) exp = MakefileProjectExporter::createForSettings (project, settings);
if (exp == nullptr) exp = AndroidProjectExporter::createForSettings (project, settings);
if (exp == nullptr) exp = XCodeProjectExporter ::createForSettings (project, settings);
if (exp == nullptr) exp = MakefileProjectExporter ::createForSettings (project, settings);
if (exp == nullptr) exp = AndroidProjectExporter ::createForSettings (project, settings);
jassert (exp != nullptr);
return exp;
@@ -115,7 +115,6 @@ ProjectExporter* ProjectExporter::createPlatformDefaultExporter (Project& projec
for (int i = 0; i < project.getNumExporters(); ++i)
{
ScopedPointer <ProjectExporter> exp (project.createExporter (i));
const int pref = exp->getLaunchPreferenceOrderForCurrentOS();
if (pref > bestPref)
@@ -210,7 +209,7 @@ void ProjectExporter::createPropertyEditors (Array <PropertyComponent*>& props)
props.getLast()->setTooltip ("The location of the Juce library folder that the " + name + " project will use to when compiling. This can be an absolute path, or relative to the jucer project folder, but it must be valid on the filesystem of the machine you use to actually do the compiling.");
OwnedArray<LibraryModule> modules;
ModuleList moduleList;
ModuleList moduleList (ModuleList::getDefaultModulesFolder (&project));
project.createRequiredModules (moduleList, modules);
for (int i = 0; i < modules.size(); ++i)
modules.getUnchecked(i)->createPropertyEditors (*this, props);


+ 1
- 1
extras/Introjucer/Source/Project Saving/jucer_ProjectSaver.h View File

@@ -61,7 +61,7 @@ public:
OwnedArray<LibraryModule> modules;
{
ModuleList moduleList;
ModuleList moduleList (ModuleList::getDefaultModulesFolder (&project));
project.createRequiredModules (moduleList, modules);
}


+ 129
- 20
extras/Introjucer/Source/Project/jucer_Module.cpp View File

@@ -31,9 +31,94 @@
//==============================================================================
ModuleList::ModuleList()
ModuleList::ModuleList (const File& modulesFolder_)
{
rescan();
rescan (modulesFolder_);
}
ModuleList::ModuleList (const ModuleList& other)
: moduleFolder (other.moduleFolder)
{
modules.addCopiesOf (other.modules);
}
ModuleList& ModuleList::operator= (const ModuleList& other)
{
moduleFolder = other.moduleFolder;
modules.clear();
modules.addCopiesOf (other.modules);
return *this;
}
bool ModuleList::operator== (const ModuleList& other) const
{
if (modules.size() != other.modules.size())
return false;
for (int i = modules.size(); --i >= 0;)
{
const Module* m1 = modules.getUnchecked(i);
const Module* m2 = other.findModuleInfo (m1->uid);
if (m2 == nullptr || *m1 != *m2)
return false;
}
return true;
}
File ModuleList::getModulesFolderForJuceOrModulesFolder (const File& f)
{
if (f.getFileName() != "modules" && f.isDirectory() && f.getChildFile ("modules").isDirectory())
return f.getChildFile ("modules");
return f;
}
File ModuleList::getDefaultModulesFolder (Project* project)
{
if (project != nullptr)
{
ScopedPointer <ProjectExporter> exp (ProjectExporter::createPlatformDefaultExporter (*project));
if (exp != nullptr)
{
File f (project->resolveFilename (exp->getJuceFolder().toString()));
f = getModulesFolderForJuceOrModulesFolder (f);
if (FileHelpers::isModulesFolder (f))
return f;
}
}
return File::getSpecialLocation (File::userHomeDirectory)
.getChildFile ("juce")
.getChildFile ("modules");
}
File ModuleList::getLocalModulesFolder (Project* project)
{
File defaultJuceFolder (getDefaultModulesFolder (project));
File f (StoredSettings::getInstance()->getProps().getValue ("lastJuceFolder", defaultJuceFolder.getFullPathName()));
f = getModulesFolderForJuceOrModulesFolder (f);
if ((! FileHelpers::isModulesFolder (f)) && FileHelpers::isModulesFolder (defaultJuceFolder))
f = defaultJuceFolder;
return f;
}
File ModuleList::getModuleFolder (const String& uid) const
{
return getModulesFolder().getChildFile (uid);
}
void ModuleList::setLocalModulesFolder (const File& file)
{
//jassert (FileHelpers::isJuceFolder (file));
StoredSettings::getInstance()->getProps().setValue ("lastJuceFolder", file.getFullPathName());
}
struct ModuleSorter
@@ -52,30 +137,38 @@ void ModuleList::sort()
void ModuleList::rescan()
{
modules.clear();
moduleFolder = StoredSettings::getInstance()->getLastKnownJuceFolder().getChildFile ("modules");
rescan (moduleFolder);
}
DirectoryIterator iter (moduleFolder, false, "*", File::findDirectories);
void ModuleList::rescan (const File& newModulesFolder)
{
modules.clear();
moduleFolder = getModulesFolderForJuceOrModulesFolder (newModulesFolder);
while (iter.next())
if (moduleFolder.isDirectory())
{
const File moduleDef (iter.getFile().getChildFile (LibraryModule::getInfoFileName()));
DirectoryIterator iter (moduleFolder, false, "*", File::findDirectories);
if (moduleDef.exists())
while (iter.next())
{
LibraryModule m (moduleDef);
jassert (m.isValid());
const File moduleDef (iter.getFile().getChildFile (LibraryModule::getInfoFileName()));
if (m.isValid())
if (moduleDef.exists())
{
Module* info = new Module();
modules.add (info);
info->uid = m.getID();
info->version = m.getVersion();
info->name = m.moduleInfo ["name"];
info->description = m.moduleInfo ["description"];
info->file = moduleDef;
LibraryModule m (moduleDef);
jassert (m.isValid());
if (m.isValid())
{
Module* info = new Module();
modules.add (info);
info->uid = m.getID();
info->version = m.getVersion();
info->name = m.moduleInfo ["name"];
info->description = m.moduleInfo ["description"];
info->file = moduleDef;
}
}
}
}
@@ -83,7 +176,7 @@ void ModuleList::rescan()
sort();
}
void ModuleList::loadFromWebsite()
bool ModuleList::loadFromWebsite()
{
modules.clear();
@@ -122,6 +215,7 @@ void ModuleList::loadFromWebsite()
}
sort();
return infoList.isArray();
}
LibraryModule* ModuleList::Module::create() const
@@ -129,6 +223,21 @@ LibraryModule* ModuleList::Module::create() const
return new LibraryModule (file);
}
bool ModuleList::Module::operator== (const Module& other) const
{
return uid == other.uid
&& version == other.version
&& name == other.name
&& description == other.description
&& file == other.file
&& url == other.url;
}
bool ModuleList::Module::operator!= (const Module& other) const
{
return ! operator== (other);
}
LibraryModule* ModuleList::loadModule (const String& uid) const
{
const Module* const m = findModuleInfo (uid);


+ 22
- 2
extras/Introjucer/Source/Project/jucer_Module.h View File

@@ -86,11 +86,17 @@ private:
class ModuleList
{
public:
ModuleList();
ModuleList (const File& modulesFolder);
ModuleList (const ModuleList&);
ModuleList& operator= (const ModuleList&);
//==============================================================================
void rescan (const File& newModulesFolder);
void rescan();
void loadFromWebsite();
File getModulesFolder() const { return moduleFolder; }
File getModuleFolder (const String& uid) const;
bool loadFromWebsite();
LibraryModule* loadModule (const String& uid) const;
@@ -105,10 +111,24 @@ public:
String uid, version, name, description;
File file;
URL url;
bool operator== (const Module&) const;
bool operator!= (const Module&) const;
};
const Module* findModuleInfo (const String& uid) const;
bool operator== (const ModuleList&) const;
//==============================================================================
static File getDefaultModulesFolder (Project* project);
static File getLocalModulesFolder (Project* project);
static void setLocalModulesFolder (const File& newFile);
static File getModulesFolderForJuceOrModulesFolder (const File& f);
//==============================================================================
OwnedArray<Module> modules;
private:


+ 2
- 1
extras/Introjucer/Source/Project/jucer_NewProjectWizard.cpp View File

@@ -25,6 +25,7 @@
#include "jucer_NewProjectWizard.h"
#include "jucer_ProjectType.h"
#include "jucer_Module.h"
//==============================================================================
class GUIAppWizard : public NewProjectWizard
@@ -451,7 +452,7 @@ Project* NewProjectWizard::runNewProjectWizard (Component* ownerWindow)
aw.addComboBox ("type", getWizards(), "Project Type");
FilenameComponent juceFolderSelector ("Juce Library Location", StoredSettings::getInstance()->getLastKnownJuceFolder(),
FilenameComponent juceFolderSelector ("Juce Library Location", ModuleList::getLocalModulesFolder (nullptr),
true, true, false, "*", String::empty, "(Please select the folder containing Juce!)");
juceFolderSelector.setSize (350, 22);


+ 0
- 12
extras/Introjucer/Source/Project/jucer_Project.cpp View File

@@ -177,18 +177,6 @@ const String Project::saveDocument (const File& file)
updateProjectSettings();
sanitiseConfigFlags();
{
ScopedPointer <ProjectExporter> exp (ProjectExporter::createPlatformDefaultExporter (*this));
if (exp != nullptr)
{
File f (resolveFilename (exp->getJuceFolder().toString()));
if (FileHelpers::isJuceFolder (f))
StoredSettings::getInstance()->setLastKnownJuceFolder (f.getFullPathName());
}
}
StoredSettings::getInstance()->recentFiles.addFile (file);
ProjectSaver saver (*this, file);


+ 46
- 31
extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp View File

@@ -19,6 +19,7 @@
//[CppHeaders] You can add your own extra header files here...
#include "../Project Saving/jucer_ProjectExporter.h"
#include "jucer_Module.h"
#include "../Application/jucer_JuceUpdater.h"
//[/CppHeaders]
#include "jucer_ProjectInformationComponent.h"
@@ -126,7 +127,7 @@ public:
ModuleSettingsPanel (Project& project_, ModuleList& moduleList_, const String& moduleID_)
: PanelBase (project_), moduleList (moduleList_), moduleID (moduleID_)
{
setBounds ("parent.width / 2 + 1, 3, parent.width - 3, parent.height - 3");
setBounds ("parent.width / 2 + 1, 31, parent.width - 3, parent.height - 3");
}
void rebuildProperties (Array <PropertyComponent*>& props)
@@ -207,7 +208,7 @@ private:
if (module != nullptr)
{
String text;
text << module->name << newLine << newLine
text << module->name << newLine << "Version: " << module->version << newLine << newLine
<< module->description;
GlyphArrangement ga;
@@ -279,16 +280,34 @@ private:
//==============================================================================
class ModulesPanel : public Component,
public ListBoxModel
public ListBoxModel,
public FilenameComponentListener,
public ButtonListener
{
public:
ModulesPanel (Project& project_)
: project (project_)
: project (project_),
moduleList (ModuleList::getLocalModulesFolder (&project)),
modulesLocation ("modules", moduleList.getModulesFolder(),
true, true, false, "*", String::empty,
"Select a folder containing your JUCE modules..."),
modulesLabel (String::empty, "Module source folder:"),
updateModulesButton ("Check for module updates...")
{
addAndMakeVisible (&modulesLocation);
modulesLocation.setBounds ("150, 3, parent.width - 180, 28");
modulesLocation.addListener (this);
modulesLabel.attachToComponent (&modulesLocation, true);
addAndMakeVisible (&updateModulesButton);
updateModulesButton.setBounds ("parent.width - 175, 3, parent.width - 4, 28");
updateModulesButton.addListener (this);
modulesList.setModel (this);
modulesList.setColour (ListBox::backgroundColourId, Colours::white.withAlpha (0.4f));
addAndMakeVisible (&modulesList);
modulesList.setBounds ("4, 3, parent.width / 2 - 4, parent.height - 3");
modulesList.setBounds ("4, 31, parent.width / 2 - 4, parent.height - 3");
}
int getNumRows()
@@ -330,36 +349,29 @@ public:
if (m != nullptr)
{
if (project.isModuleEnabled (m->uid))
{
project.removeModule (m->uid);
}
else
{
const StringArray extraDepsNeeded (getExtraDependenciesNeeded (project, moduleList, *m));
/* if (extraDepsNeeded.size() > 0)
{
if (AlertWindow::showOkCancelBox (AlertWindow::NoIcon,
"Module Dependencies",
"The '" + m->uid + "' module requires the following dependencies:\n"
+ extraDepsNeeded.joinIntoString (", ") + "\n\nDo you want to add all these to your project?"))
{
project.addModule (m->uid);
for (int i = extraDepsNeeded.size(); --i >= 0;)
project.addModule (extraDepsNeeded[i]);
}
}
else*/
{
project.addModule (m->uid);
}
}
project.addModule (m->uid);
}
refresh();
}
void filenameComponentChanged (FilenameComponent*)
{
moduleList.rescan (modulesLocation.getCurrentFile());
modulesLocation.setCurrentFile (moduleList.getModulesFolder(), false, false);
ModuleList::setLocalModulesFolder (moduleList.getModulesFolder());
modulesList.updateContent();
}
void buttonClicked (Button*)
{
JuceUpdater::show (moduleList, getTopLevelComponent());
filenameComponentChanged (nullptr);
}
void listBoxItemClicked (int row, const MouseEvent& e)
{
if (e.x < modulesList.getRowHeight())
@@ -397,6 +409,9 @@ public:
private:
Project& project;
ModuleList moduleList;
FilenameComponent modulesLocation;
Label modulesLabel;
TextButton updateModulesButton;
ListBox modulesList;
ScopedPointer<ModuleSettingsPanel> settings;
};
@@ -440,16 +455,16 @@ ProjectInformationComponent::ProjectInformationComponent (Project& project_)
//[UserPreSize]
rebuildConfigTabs();
#if JUCE_MAC || JUCE_WINDOWS
#if JUCE_MAC || JUCE_WINDOWS
openProjectButton.setCommandToTrigger (commandManager, CommandIDs::openInIDE, true);
openProjectButton.setButtonText (commandManager->getNameOfCommand (CommandIDs::openInIDE));
saveAndOpenButton.setCommandToTrigger (commandManager, CommandIDs::saveAndOpenInIDE, true);
saveAndOpenButton.setButtonText (commandManager->getNameOfCommand (CommandIDs::saveAndOpenInIDE));
#else
#else
openProjectButton.setVisible (false);
saveAndOpenButton.setVisible (false);
#endif
#endif
//[/UserPreSize]
setSize (836, 427);


+ 8
- 15
extras/Introjucer/Source/Utility/jucer_FileHelpers.cpp View File

@@ -150,10 +150,16 @@ namespace FileHelpers
bool isJuceFolder (const File& folder)
{
return folder.getFileName().containsIgnoreCase ("juce")
&& folder.getChildFile ("modules").isDirectory();
&& isModulesFolder (folder.getChildFile ("modules"));
}
static File lookInFolderForJuceFolder (const File& folder)
bool isModulesFolder (const File& folder)
{
return folder.getFileName().equalsIgnoreCase ("modules")
&& folder.isDirectory();
}
File lookInFolderForJuceFolder (const File& folder)
{
for (DirectoryIterator di (folder, false, "*juce*", File::findDirectories); di.next();)
{
@@ -182,17 +188,4 @@ namespace FileHelpers
return File::nonexistent;
}
File findDefaultJuceFolder()
{
File f = findParentJuceFolder (File::getSpecialLocation (File::currentApplicationFile));
if (! f.exists())
f = lookInFolderForJuceFolder (File::getSpecialLocation (File::userHomeDirectory));
if (! f.exists())
f = lookInFolderForJuceFolder (File::getSpecialLocation (File::userDocumentsDirectory));
return f;
}
}

+ 2
- 1
extras/Introjucer/Source/Utility/jucer_FileHelpers.h View File

@@ -46,8 +46,9 @@ namespace FileHelpers
//==============================================================================
bool isJuceFolder (const File& folder);
bool isModulesFolder (const File& folder);
File findParentJuceFolder (const File& file);
File findDefaultJuceFolder();
File lookInFolderForJuceFolder (const File& folder);
}
//==============================================================================


+ 0
- 17
extras/Introjucer/Source/Utility/jucer_StoredSettings.cpp View File

@@ -143,23 +143,6 @@ void StoredSettings::setLastProjects (const Array<File>& files)
props->setValue ("lastProjects", s.joinIntoString ("|"));
}
File StoredSettings::getLastKnownJuceFolder() const
{
File defaultJuceFolder (FileHelpers::findDefaultJuceFolder());
File f (props->getValue ("lastJuceFolder", defaultJuceFolder.getFullPathName()));
if ((! FileHelpers::isJuceFolder (f)) && FileHelpers::isJuceFolder (defaultJuceFolder))
f = defaultJuceFolder;
return f;
}
void StoredSettings::setLastKnownJuceFolder (const File& file)
{
jassert (FileHelpers::isJuceFolder (file));
props->setValue ("lastJuceFolder", file.getFullPathName());
}
const StringArray& StoredSettings::getFontNames()
{
if (fontNames.size() == 0)


+ 0
- 3
extras/Introjucer/Source/Utility/jucer_StoredSettings.h View File

@@ -50,9 +50,6 @@ public:
Array<File> getLastProjects() const;
void setLastProjects (const Array<File>& files);
File getLastKnownJuceFolder() const;
void setLastKnownJuceFolder (const File& file);
const StringArray& getFontNames();
//==============================================================================


+ 1
- 1
modules/juce_core/system/juce_StandardHeader.h View File

@@ -33,7 +33,7 @@
*/
#define JUCE_MAJOR_VERSION 2
#define JUCE_MINOR_VERSION 0
#define JUCE_BUILDNUMBER 4
#define JUCE_BUILDNUMBER 5
/** Current Juce version number.


Loading…
Cancel
Save