See BREAKING-CHANGES.txt for more details.v6.1.6
@@ -4,6 +4,31 @@ JUCE breaking changes | |||
Develop | |||
======= | |||
Change | |||
------ | |||
The default value of JUCE_MODAL_LOOPS_PERMITTED has been changed from 1 to 0. | |||
Possible Issues | |||
--------------- | |||
With JUCE_MODAL_LOOPS_PERMITTED set to 0 code that previously relied upon modal | |||
loops will need to be rewritten to use asynchronous versions of the modal | |||
functions. There is no non-modal alternative to | |||
AlterWindow::showNativeDialogBox and the previously modal behaviour of the | |||
MultiDocumentPanel destructor has changed. | |||
Workaround | |||
---------- | |||
Set JUCE_MODAL_LOOPS_PERMITTED back to 1. | |||
Rationale | |||
--------- | |||
Modal operations are a frequent source of problems, particularly when used in | |||
plug-ins. On Android modal loops are not possible, so people wanting to target | |||
Android often have an unwelcome surprise when then have to rewrite what they | |||
assumed to be platform independent code. Changing the default addresses these | |||
problems. | |||
Change | |||
------ | |||
The minimum supported C++ standard is now C++14 and the oldest supported | |||
@@ -590,14 +590,13 @@ private: | |||
if (fileChooser != nullptr) | |||
return; | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | |||
{ | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
RuntimePermissions::request (RuntimePermissions::readExternalStorage, | |||
[safeThis] (bool granted) mutable | |||
{ | |||
if (granted) | |||
if (safeThis != nullptr && granted) | |||
safeThis->openFile(); | |||
}); | |||
return; | |||
@@ -606,22 +605,19 @@ private: | |||
fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | |||
fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | |||
[safeThis] (const FileChooser& fc) mutable | |||
[this] (const FileChooser& fc) mutable | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
if (fc.getURLResults().size() > 0) | |||
{ | |||
auto u = fc.getURLResult(); | |||
if (! safeThis->audioFileReader.loadURL (u)) | |||
if (! audioFileReader.loadURL (u)) | |||
NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | |||
else | |||
safeThis->thumbnailComp.setCurrentURL (u); | |||
thumbnailComp.setCurrentURL (u); | |||
} | |||
safeThis->fileChooser = nullptr; | |||
fileChooser = nullptr; | |||
}, nullptr); | |||
} | |||
@@ -498,14 +498,13 @@ private: | |||
{ | |||
if (btn == &chooseFileButton && fileChooser.get() == nullptr) | |||
{ | |||
SafePointer<AudioPlaybackDemo> safeThis (this); | |||
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | |||
{ | |||
SafePointer<AudioPlaybackDemo> safeThis (this); | |||
RuntimePermissions::request (RuntimePermissions::readExternalStorage, | |||
[safeThis] (bool granted) mutable | |||
{ | |||
if (granted) | |||
if (safeThis != nullptr && granted) | |||
safeThis->buttonClicked (&safeThis->chooseFileButton); | |||
}); | |||
return; | |||
@@ -516,16 +515,16 @@ private: | |||
fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | |||
fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | |||
[safeThis] (const FileChooser& fc) mutable | |||
[this] (const FileChooser& fc) mutable | |||
{ | |||
if (safeThis != nullptr && fc.getURLResults().size() > 0) | |||
if (fc.getURLResults().size() > 0) | |||
{ | |||
auto u = fc.getURLResult(); | |||
safeThis->showAudioResource (std::move (u)); | |||
showAudioResource (std::move (u)); | |||
} | |||
safeThis->fileChooser = nullptr; | |||
fileChooser = nullptr; | |||
}, nullptr); | |||
} | |||
else | |||
@@ -590,14 +590,13 @@ private: | |||
if (fileChooser != nullptr) | |||
return; | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | |||
{ | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
RuntimePermissions::request (RuntimePermissions::readExternalStorage, | |||
[safeThis] (bool granted) mutable | |||
{ | |||
if (granted) | |||
if (safeThis != nullptr && granted) | |||
safeThis->openFile(); | |||
}); | |||
return; | |||
@@ -606,22 +605,19 @@ private: | |||
fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | |||
fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | |||
[safeThis] (const FileChooser& fc) mutable | |||
[this] (const FileChooser& fc) mutable | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
if (fc.getURLResults().size() > 0) | |||
{ | |||
auto u = fc.getURLResult(); | |||
if (! safeThis->audioFileReader.loadURL (u)) | |||
if (! audioFileReader.loadURL (u)) | |||
NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | |||
else | |||
safeThis->thumbnailComp.setCurrentURL (u); | |||
thumbnailComp.setCurrentURL (u); | |||
} | |||
safeThis->fileChooser = nullptr; | |||
fileChooser = nullptr; | |||
}, nullptr); | |||
} | |||
@@ -141,10 +141,21 @@ public: | |||
nativeButton.setButtonText ("Use Native Windows"); | |||
nativeButton.onClick = [this] { getLookAndFeel().setUsingNativeAlertWindows (nativeButton.getToggleState()); }; | |||
StringArray windowNames { "Plain Alert Window", "Alert Window With Warning Icon", "Alert Window With Info Icon", "Alert Window With Question Icon", | |||
"OK Cancel Alert Window", "Alert Window With Extra Components", "CalloutBox", "Thread With Progress Window", | |||
"'Load' File Browser", "'Load' File Browser With Image Preview", "'Choose Directory' File Browser", "'Save' File Browser", | |||
"Share Text", "Share Files", "Share Images" }; | |||
StringArray windowNames { "Plain Alert Window", | |||
"Alert Window With Warning Icon", | |||
"Alert Window With Info Icon", | |||
"Alert Window With Question Icon", | |||
"OK Cancel Alert Window", | |||
"Alert Window With Extra Components", | |||
"CalloutBox", | |||
"Thread With Progress Window", | |||
"'Load' File Browser", | |||
"'Load' File Browser With Image Preview", | |||
"'Choose Directory' File Browser", | |||
"'Save' File Browser", | |||
"Share Text", | |||
"Share Files", | |||
"Share Images" }; | |||
// warn in case we add any windows | |||
jassert (windowNames.size() == numDialogs); | |||
@@ -207,11 +218,42 @@ private: | |||
OwnedArray<TextButton> windowButtons; | |||
ToggleButton nativeButton; | |||
static void alertBoxResultChosen (int result, DialogsDemo*) | |||
struct AlertBoxResultChosen | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "Alert Box", | |||
"Result code: " + String (result)); | |||
} | |||
void operator() (int result) const noexcept | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, | |||
"Alert Box", | |||
"Result code: " + String (result)); | |||
} | |||
}; | |||
struct AsyncAlertBoxResultChosen | |||
{ | |||
void operator() (int result) const noexcept | |||
{ | |||
auto& aw = *demo.asyncAlertWindow; | |||
aw.exitModalState (result); | |||
aw.setVisible (false); | |||
if (result == 0) | |||
{ | |||
AlertBoxResultChosen{} (result); | |||
return; | |||
} | |||
auto optionIndexChosen = aw.getComboBoxComponent ("option")->getSelectedItemIndex(); | |||
auto text = aw.getTextEditorContents ("text"); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "Alert Box", | |||
"Result code: " + String (result) + newLine | |||
+ "Option index chosen: " + String (optionIndexChosen) + newLine | |||
+ "Text: " + text); | |||
} | |||
DialogsDemo& demo; | |||
}; | |||
void showWindow (Component& button, DialogType type) | |||
{ | |||
@@ -232,7 +274,7 @@ private: | |||
AlertWindow::showOkCancelBox (AlertWindow::QuestionIcon, "This is an ok/cancel AlertWindow", | |||
"And this is the AlertWindow's message. Blah blah blah blah blah blah blah blah blah blah blah blah blah.", | |||
{}, {}, {}, | |||
ModalCallbackFunction::forComponent (alertBoxResultChosen, this)); | |||
ModalCallbackFunction::create (AlertBoxResultChosen{})); | |||
} | |||
else if (type == calloutBoxWindow) | |||
{ | |||
@@ -247,31 +289,16 @@ private: | |||
} | |||
else if (type == extraComponentsAlertWindow) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
// Modal loops are extremely dangerous. Do not copy the code below unless you are absolutely | |||
// certain you are aware of all the many complicated things that can go catastrophically | |||
// wrong. Read the documentation for Component::runModalLoop. If you find you are using code | |||
// similar to this you should refactor things to remove it. | |||
AlertWindow w ("AlertWindow demo..", | |||
"This AlertWindow has a couple of extra components added to show how to add drop-down lists and text entry boxes.", | |||
AlertWindow::QuestionIcon); | |||
w.addTextEditor ("text", "enter some text here", "text field:"); | |||
w.addComboBox ("option", { "option 1", "option 2", "option 3", "option 4" }, "some options"); | |||
w.addButton ("OK", 1, KeyPress (KeyPress::returnKey, 0, 0)); | |||
w.addButton ("Cancel", 0, KeyPress (KeyPress::escapeKey, 0, 0)); | |||
if (w.runModalLoop() != 0) // is they picked 'ok' | |||
{ | |||
// this is the item they chose in the drop-down list.. | |||
auto optionIndexChosen = w.getComboBoxComponent ("option")->getSelectedItemIndex(); | |||
ignoreUnused (optionIndexChosen); | |||
asyncAlertWindow = std::make_unique<AlertWindow> ("AlertWindow demo..", | |||
"This AlertWindow has a couple of extra components added to show how to add drop-down lists and text entry boxes.", | |||
AlertWindow::QuestionIcon); | |||
// this is the text they entered.. | |||
auto text = w.getTextEditorContents ("text"); | |||
} | |||
#endif | |||
asyncAlertWindow->addTextEditor ("text", "enter some text here", "text field:"); | |||
asyncAlertWindow->addComboBox ("option", { "option 1", "option 2", "option 3", "option 4" }, "some options"); | |||
asyncAlertWindow->addButton ("OK", 1, KeyPress (KeyPress::returnKey, 0, 0)); | |||
asyncAlertWindow->addButton ("Cancel", 0, KeyPress (KeyPress::escapeKey, 0, 0)); | |||
asyncAlertWindow->enterModalState (true, ModalCallbackFunction::create (AsyncAlertBoxResultChosen { *this })); | |||
} | |||
else if (type == progressWindow) | |||
{ | |||
@@ -461,6 +488,7 @@ private: | |||
ImagePreviewComponent imagePreview; | |||
std::unique_ptr<FileChooser> fc; | |||
std::unique_ptr<AlertWindow> asyncAlertWindow; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DialogsDemo) | |||
}; |
@@ -110,14 +110,12 @@ public: | |||
// not interested in this for now | |||
} | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
File getSuggestedSaveAsFile (const File&) override | |||
{ | |||
return File::getSpecialLocation (File::userDesktopDirectory) | |||
.getChildFile (getName()) | |||
.withFileExtension ("jnote"); | |||
} | |||
#endif | |||
private: | |||
Value textValueObject; | |||
@@ -138,23 +136,19 @@ private: | |||
class DemoMultiDocumentPanel : public MultiDocumentPanel | |||
{ | |||
public: | |||
DemoMultiDocumentPanel() {} | |||
DemoMultiDocumentPanel() = default; | |||
~DemoMultiDocumentPanel() override | |||
void tryToCloseDocumentAsync (Component* component, std::function<void (bool)> callback) override | |||
{ | |||
closeAllDocuments (true); | |||
} | |||
bool tryToCloseDocument (Component* component) override | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
if (auto* note = dynamic_cast<Note*> (component)) | |||
return note->saveIfNeededAndUserAgrees() != FileBasedDocument::failedToWriteToFile; | |||
#else | |||
ignoreUnused (component); | |||
#endif | |||
return true; | |||
{ | |||
SafePointer<DemoMultiDocumentPanel> parent { this }; | |||
note->saveIfNeededAndUserAgreesAsync ([parent, callback] (FileBasedDocument::SaveResult result) | |||
{ | |||
if (parent != nullptr) | |||
callback (result == FileBasedDocument::savedOk); | |||
}); | |||
} | |||
} | |||
private: | |||
@@ -180,6 +174,16 @@ public: | |||
addNoteButton.onClick = [this] { addNote ("Note " + String (multiDocumentPanel.getNumDocuments() + 1), "Hello World!"); }; | |||
addAndMakeVisible (addNoteButton); | |||
closeApplicationButton.onClick = [this] | |||
{ | |||
multiDocumentPanel.closeAllDocumentsAsync (true, [] (bool allSaved) | |||
{ | |||
if (allSaved) | |||
JUCEApplicationBase::quit(); | |||
}); | |||
}; | |||
addAndMakeVisible (closeApplicationButton); | |||
addAndMakeVisible (multiDocumentPanel); | |||
multiDocumentPanel.setBackgroundColour (Colours::transparentBlack); | |||
@@ -200,8 +204,9 @@ public: | |||
auto area = getLocalBounds(); | |||
auto buttonArea = area.removeFromTop (28).reduced (2); | |||
addNoteButton .setBounds (buttonArea.removeFromRight (150)); | |||
showInTabsButton.setBounds (buttonArea); | |||
closeApplicationButton.setBounds (buttonArea.removeFromRight (150)); | |||
addNoteButton .setBounds (buttonArea.removeFromRight (150)); | |||
showInTabsButton .setBounds (buttonArea); | |||
multiDocumentPanel.setBounds (area); | |||
} | |||
@@ -235,11 +240,6 @@ public: | |||
} | |||
private: | |||
ToggleButton showInTabsButton { "Show with tabs" }; | |||
TextButton addNoteButton { "Create a new note" }; | |||
DemoMultiDocumentPanel multiDocumentPanel; | |||
void updateLayoutMode() | |||
{ | |||
multiDocumentPanel.setLayoutMode (showInTabsButton.getToggleState() ? MultiDocumentPanel::MaximisedWindowsWithTabs | |||
@@ -261,5 +261,11 @@ private: | |||
createNotesForFiles (files); | |||
} | |||
ToggleButton showInTabsButton { "Show with tabs" }; | |||
TextButton addNoteButton { "Create a new note" }, | |||
closeApplicationButton { "Close app" }; | |||
DemoMultiDocumentPanel multiDocumentPanel; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MDIDemo) | |||
}; |
@@ -1103,25 +1103,25 @@ private: | |||
void selectTexture (int itemID) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
if (itemID == 1000) | |||
{ | |||
auto lastLocation = File::getSpecialLocation (File::userPicturesDirectory); | |||
textureFileChooser = std::make_unique<FileChooser> ("Choose an image to open...", | |||
File::getSpecialLocation (File::userPicturesDirectory), | |||
"*.jpg;*.jpeg;*.png;*.gif"); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; | |||
FileChooser fc ("Choose an image to open...", lastLocation, "*.jpg;*.jpeg;*.png;*.gif"); | |||
if (fc.browseForFileToOpen()) | |||
textureFileChooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
lastLocation = fc.getResult(); | |||
if (fc.getResult() == File{}) | |||
return; | |||
textures.add (new OpenGLUtils::TextureFromFile (fc.getResult())); | |||
updateTexturesList(); | |||
textureBox.setSelectedId (textures.size()); | |||
} | |||
}); | |||
} | |||
else | |||
#endif | |||
{ | |||
if (auto* t = textures[itemID - 1]) | |||
demo.setTexture (t); | |||
@@ -1135,10 +1135,8 @@ private: | |||
for (int i = 0; i < textures.size(); ++i) | |||
textureBox.addItem (textures.getUnchecked (i)->name, i + 1); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
textureBox.addSeparator(); | |||
textureBox.addItem ("Load from a file...", 1000); | |||
#endif | |||
} | |||
void updateShader() | |||
@@ -1208,6 +1206,8 @@ private: | |||
OwnedArray<OpenGLUtils::DemoTexture> textures; | |||
std::unique_ptr<FileChooser> textureFileChooser; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoControlsOverlay) | |||
}; | |||
@@ -590,14 +590,13 @@ private: | |||
if (fileChooser != nullptr) | |||
return; | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | |||
{ | |||
SafePointer<AudioPlayerHeader> safeThis (this); | |||
RuntimePermissions::request (RuntimePermissions::readExternalStorage, | |||
[safeThis] (bool granted) mutable | |||
{ | |||
if (granted) | |||
if (safeThis != nullptr && granted) | |||
safeThis->openFile(); | |||
}); | |||
return; | |||
@@ -606,22 +605,19 @@ private: | |||
fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | |||
fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | |||
[safeThis] (const FileChooser& fc) mutable | |||
[this] (const FileChooser& fc) mutable | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
if (fc.getURLResults().size() > 0) | |||
{ | |||
auto u = fc.getURLResult(); | |||
if (! safeThis->audioFileReader.loadURL (u)) | |||
if (! audioFileReader.loadURL (u)) | |||
NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | |||
else | |||
safeThis->thumbnailComp.setCurrentURL (u); | |||
thumbnailComp.setCurrentURL (u); | |||
} | |||
safeThis->fileChooser = nullptr; | |||
fileChooser = nullptr; | |||
}, nullptr); | |||
} | |||
@@ -189,23 +189,45 @@ void MainHostWindow::tryToQuitApplication() | |||
// to flush any GUI events that may have been in transit before the app forces them to | |||
// be unloaded | |||
new AsyncQuitRetrier(); | |||
return; | |||
} | |||
else if (ModalComponentManager::getInstance()->cancelAllModalComponents()) | |||
if (ModalComponentManager::getInstance()->cancelAllModalComponents()) | |||
{ | |||
new AsyncQuitRetrier(); | |||
return; | |||
} | |||
#if JUCE_ANDROID || JUCE_IOS | |||
else if (graphHolder == nullptr || graphHolder->graph->saveDocument (PluginGraph::getDefaultGraphDocumentOnMobile())) | |||
#else | |||
else if (graphHolder == nullptr || graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) | |||
#endif | |||
if (graphHolder != nullptr) | |||
{ | |||
// Some plug-ins do not want [NSApp stop] to be called | |||
// before the plug-ins are not deallocated. | |||
graphHolder->releaseGraph(); | |||
auto releaseAndQuit = [this] | |||
{ | |||
// Some plug-ins do not want [NSApp stop] to be called | |||
// before the plug-ins are not deallocated. | |||
graphHolder->releaseGraph(); | |||
JUCEApplication::quit(); | |||
}; | |||
#if JUCE_ANDROID || JUCE_IOS | |||
if (graphHolder->graph->saveDocument (PluginGraph::getDefaultGraphDocumentOnMobile())) | |||
releaseAndQuit(); | |||
#else | |||
SafePointer<MainHostWindow> parent { this }; | |||
graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent, releaseAndQuit] (FileBasedDocument::SaveResult r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == FileBasedDocument::savedOk) | |||
releaseAndQuit(); | |||
}); | |||
#endif | |||
JUCEApplication::quit(); | |||
return; | |||
} | |||
JUCEApplication::quit(); | |||
} | |||
void MainHostWindow::changeListenerCallback (ChangeBroadcaster* changed) | |||
@@ -329,9 +351,20 @@ void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/ | |||
->getValue ("recentFilterGraphFiles")); | |||
if (graphHolder != nullptr) | |||
{ | |||
if (auto* graph = graphHolder->graph.get()) | |||
if (graph != nullptr && graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) | |||
graph->loadFrom (recentFiles.getFile (menuItemID - 100), true); | |||
{ | |||
SafePointer<MainHostWindow> parent { this }; | |||
graph->saveIfNeededAndUserAgreesAsync ([parent, recentFiles, menuItemID] (FileBasedDocument::SaveResult r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == FileBasedDocument::savedOk) | |||
parent->graphHolder->graph->loadFrom (recentFiles.getFile (menuItemID - 100), true); | |||
}); | |||
} | |||
} | |||
} | |||
#endif | |||
else if (menuItemID >= 200 && menuItemID < 210) | |||
@@ -492,23 +525,43 @@ bool MainHostWindow::perform (const InvocationInfo& info) | |||
{ | |||
#if ! (JUCE_IOS || JUCE_ANDROID) | |||
case CommandIDs::newFile: | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr && graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) | |||
graphHolder->graph->newDocument(); | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr) | |||
{ | |||
SafePointer<MainHostWindow> parent { this }; | |||
graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent] (FileBasedDocument::SaveResult r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == FileBasedDocument::savedOk) | |||
parent->graphHolder->graph->newDocument(); | |||
}); | |||
} | |||
break; | |||
case CommandIDs::open: | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr && graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) | |||
graphHolder->graph->loadFromUserSpecifiedFile (true); | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr) | |||
{ | |||
SafePointer<MainHostWindow> parent { this }; | |||
graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent] (FileBasedDocument::SaveResult r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == FileBasedDocument::savedOk) | |||
parent->graphHolder->graph->loadFromUserSpecifiedFileAsync (true, [] (Result) {}); | |||
}); | |||
} | |||
break; | |||
case CommandIDs::save: | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr) | |||
graphHolder->graph->save (true, true); | |||
graphHolder->graph->saveAsync (true, true, nullptr); | |||
break; | |||
case CommandIDs::saveAs: | |||
if (graphHolder != nullptr && graphHolder->graph != nullptr) | |||
graphHolder->graph->saveAs (File(), true, true, true); | |||
graphHolder->graph->saveAsAsync ({}, true, true, true, nullptr); | |||
break; | |||
#endif | |||
@@ -630,11 +683,22 @@ void MainHostWindow::filesDropped (const StringArray& files, int x, int y) | |||
if (graphHolder != nullptr) | |||
{ | |||
#if ! (JUCE_ANDROID || JUCE_IOS) | |||
if (files.size() == 1 && File (files[0]).hasFileExtension (PluginGraph::getFilenameSuffix())) | |||
File firstFile { files[0] }; | |||
if (files.size() == 1 && firstFile.hasFileExtension (PluginGraph::getFilenameSuffix())) | |||
{ | |||
if (auto* g = graphHolder->graph.get()) | |||
if (g->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk) | |||
g->loadFrom (File (files[0]), true); | |||
{ | |||
SafePointer<MainHostWindow> parent; | |||
g->saveIfNeededAndUserAgreesAsync ([parent, g, firstFile] (FileBasedDocument::SaveResult r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == FileBasedDocument::savedOk) | |||
g->loadFrom (firstFile, true); | |||
}); | |||
} | |||
} | |||
else | |||
#endif | |||
@@ -99,25 +99,34 @@ public: | |||
{ | |||
createProjectButton.onClick = [this] | |||
{ | |||
FileChooser fc ("Save Project", NewProjectWizard::getLastWizardFolder()); | |||
chooser = std::make_unique<FileChooser> ("Save Project", NewProjectWizard::getLastWizardFolder()); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (fc.browseForDirectory()) | |||
chooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
auto dir = fc.getResult(); | |||
if (auto project = NewProjectWizard::createNewProject (projectTemplate, | |||
dir.getChildFile (projectNameValue.get().toString()), | |||
projectNameValue.get(), | |||
modulesValue.get(), | |||
exportersValue.get(), | |||
fileOptionsValue.get(), | |||
modulePathValue.getCurrentValue(), | |||
modulePathValue.getWrappedValueWithDefault().isUsingDefault())) | |||
if (dir == File{}) | |||
return; | |||
SafePointer<TemplateComponent> safeThis { this }; | |||
NewProjectWizard::createNewProject (projectTemplate, | |||
dir.getChildFile (projectNameValue.get().toString()), | |||
projectNameValue.get(), | |||
modulesValue.get(), | |||
exportersValue.get(), | |||
fileOptionsValue.get(), | |||
modulePathValue.getCurrentValue(), | |||
modulePathValue.getWrappedValueWithDefault().isUsingDefault(), | |||
[safeThis, dir] (std::unique_ptr<Project> project) | |||
{ | |||
projectCreatedCallback (std::move (project)); | |||
if (safeThis == nullptr) | |||
return; | |||
safeThis->projectCreatedCallback (std::move (project)); | |||
getAppSettings().lastWizardFolder = dir; | |||
} | |||
} | |||
}); | |||
}); | |||
}; | |||
addAndMakeVisible (createProjectButton); | |||
@@ -150,6 +159,7 @@ public: | |||
private: | |||
NewProjectTemplates::ProjectTemplate projectTemplate; | |||
std::unique_ptr<FileChooser> chooser; | |||
std::function<void (std::unique_ptr<Project>)> projectCreatedCallback; | |||
ItemHeader header; | |||
@@ -220,63 +220,91 @@ File NewProjectWizard::getLastWizardFolder() | |||
return lastFolderFallback; | |||
} | |||
std::unique_ptr<Project> NewProjectWizard::createNewProject (const NewProjectTemplates::ProjectTemplate& projectTemplate, | |||
const File& targetFolder, const String& name, var modules, var exporters, var fileOptions, | |||
const String& modulePath, bool useGlobalModulePath) | |||
static void displayFailedFilesMessage (const StringArray& failedFiles) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Errors in Creating Project!"), | |||
TRANS("The following files couldn't be written:") | |||
+ "\n\n" | |||
+ failedFiles.joinIntoString ("\n", 0, 10)); | |||
} | |||
template <typename Callback> | |||
static void prepareDirectory (const File& targetFolder, Callback&& callback) | |||
{ | |||
StringArray failedFiles; | |||
if (! targetFolder.exists()) | |||
{ | |||
if (! targetFolder.createDirectory()) | |||
failedFiles.add (targetFolder.getFullPathName()); | |||
{ | |||
displayFailedFilesMessage ({ targetFolder.getFullPathName() }); | |||
return; | |||
} | |||
} | |||
else if (FileHelpers::containsAnyNonHiddenFiles (targetFolder)) | |||
{ | |||
if (! AlertWindow::showOkCancelBox (AlertWindow::InfoIcon, | |||
TRANS("New JUCE Project"), | |||
TRANS("You chose the folder:\n\nXFLDRX\n\n").replace ("XFLDRX", targetFolder.getFullPathName()) | |||
+ TRANS("This folder isn't empty - are you sure you want to create the project there?") | |||
+ "\n\n" | |||
+ TRANS("Any existing files with the same names may be overwritten by the new files."))) | |||
{ | |||
return nullptr; | |||
} | |||
AlertWindow::showOkCancelBox (AlertWindow::InfoIcon, | |||
TRANS("New JUCE Project"), | |||
TRANS("You chose the folder:\n\nXFLDRX\n\n").replace ("XFLDRX", targetFolder.getFullPathName()) | |||
+ TRANS("This folder isn't empty - are you sure you want to create the project there?") | |||
+ "\n\n" | |||
+ TRANS("Any existing files with the same names may be overwritten by the new files."), | |||
{}, | |||
{}, | |||
nullptr, | |||
ModalCallbackFunction::create ([callback] (int result) | |||
{ | |||
if (result != 0) | |||
callback(); | |||
})); | |||
return; | |||
} | |||
auto project = std::make_unique<Project> (targetFolder.getChildFile (File::createLegalFileName (name)) | |||
.withFileExtension (Project::projectFileExtension)); | |||
callback(); | |||
} | |||
if (failedFiles.isEmpty()) | |||
void NewProjectWizard::createNewProject (const NewProjectTemplates::ProjectTemplate& projectTemplate, | |||
const File& targetFolder, const String& name, var modules, var exporters, var fileOptions, | |||
const String& modulePath, bool useGlobalModulePath, | |||
std::function<void (std::unique_ptr<Project>)> callback) | |||
{ | |||
prepareDirectory (targetFolder, [=] | |||
{ | |||
auto project = std::make_unique<Project> (targetFolder.getChildFile (File::createLegalFileName (name)) | |||
.withFileExtension (Project::projectFileExtension)); | |||
doBasicProjectSetup (*project, projectTemplate, name); | |||
StringArray failedFiles; | |||
if (addFiles (*project, projectTemplate, name, fileOptions, failedFiles)) | |||
{ | |||
addExporters (*project, *exporters.getArray()); | |||
addModules (*project, *modules.getArray(), modulePath, useGlobalModulePath); | |||
if (project->save (false, true) == FileBasedDocument::savedOk) | |||
{ | |||
project->setChangedFlag (false); | |||
project->loadFrom (project->getFile(), true); | |||
} | |||
else | |||
auto sharedProject = std::make_shared<std::unique_ptr<Project>> (std::move (project)); | |||
(*sharedProject)->saveAsync (false, true, [sharedProject, failedFiles, callback] (FileBasedDocument::SaveResult r) | |||
{ | |||
failedFiles.add (project->getFile().getFullPathName()); | |||
} | |||
auto uniqueProject = std::move (*sharedProject.get()); | |||
if (r == FileBasedDocument::savedOk) | |||
{ | |||
uniqueProject->setChangedFlag (false); | |||
uniqueProject->loadFrom (uniqueProject->getFile(), true); | |||
callback (std::move (uniqueProject)); | |||
return; | |||
} | |||
auto failedFilesCopy = failedFiles; | |||
failedFilesCopy.add (uniqueProject->getFile().getFullPathName()); | |||
displayFailedFilesMessage (failedFilesCopy); | |||
}); | |||
return; | |||
} | |||
} | |||
if (! failedFiles.isEmpty()) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Errors in Creating Project!"), | |||
TRANS("The following files couldn't be written:") | |||
+ "\n\n" | |||
+ failedFiles.joinIntoString ("\n", 0, 10)); | |||
return nullptr; | |||
} | |||
return project; | |||
displayFailedFilesMessage (failedFiles); | |||
}); | |||
} |
@@ -32,7 +32,8 @@ namespace NewProjectWizard | |||
{ | |||
File getLastWizardFolder(); | |||
std::unique_ptr<Project> createNewProject (const NewProjectTemplates::ProjectTemplate& projectTemplate, | |||
const File& targetFolder, const String& name, var modules, var exporters, var fileOptions, | |||
const String& modulePath, bool useGlobalModulePath); | |||
void createNewProject (const NewProjectTemplates::ProjectTemplate& projectTemplate, | |||
const File& targetFolder, const String& name, var modules, var exporters, var fileOptions, | |||
const String& modulePath, bool useGlobalModulePath, | |||
std::function<void (std::unique_ptr<Project>)> callback); | |||
} |
@@ -184,40 +184,52 @@ private: | |||
void saveScheme (bool isExit) | |||
{ | |||
FileChooser fc ("Select a file in which to save this colour-scheme...", | |||
getAppSettings().appearance.getSchemesFolder() | |||
.getNonexistentChildFile ("Scheme", AppearanceSettings::getSchemeFileSuffix()), | |||
AppearanceSettings::getSchemeFileWildCard()); | |||
if (fc.browseForFileToSave (true)) | |||
chooser = std::make_unique<FileChooser> ("Select a file in which to save this colour-scheme...", | |||
getAppSettings().appearance.getSchemesFolder() | |||
.getNonexistentChildFile ("Scheme", AppearanceSettings::getSchemeFileSuffix()), | |||
AppearanceSettings::getSchemeFileWildCard()); | |||
auto chooserFlags = FileBrowserComponent::saveMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::warnAboutOverwriting; | |||
chooser->launchAsync (chooserFlags, [this, isExit] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
{ | |||
if (isExit) | |||
restorePreviousScheme(); | |||
return; | |||
} | |||
File file (fc.getResult().withFileExtension (AppearanceSettings::getSchemeFileSuffix())); | |||
getAppSettings().appearance.writeToFile (file); | |||
getAppSettings().appearance.refreshPresetSchemeList(); | |||
saveSchemeState(); | |||
ProjucerApplication::getApp().selectEditorColourSchemeWithName (file.getFileNameWithoutExtension()); | |||
} | |||
else if (isExit) | |||
{ | |||
restorePreviousScheme(); | |||
} | |||
}); | |||
} | |||
void loadScheme() | |||
{ | |||
FileChooser fc ("Please select a colour-scheme file to load...", | |||
getAppSettings().appearance.getSchemesFolder(), | |||
AppearanceSettings::getSchemeFileWildCard()); | |||
chooser = std::make_unique<FileChooser> ("Please select a colour-scheme file to load...", | |||
getAppSettings().appearance.getSchemesFolder(), | |||
AppearanceSettings::getSchemeFileWildCard()); | |||
auto chooserFlags = FileBrowserComponent::openMode | |||
| FileBrowserComponent::canSelectFiles; | |||
if (fc.browseForFileToOpen()) | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
if (getAppSettings().appearance.readFromFile (fc.getResult())) | |||
{ | |||
rebuildProperties(); | |||
saveSchemeState(); | |||
} | |||
} | |||
}); | |||
} | |||
void lookAndFeelChanged() override | |||
@@ -264,6 +276,7 @@ private: | |||
appearance.getColourValue (colourNames[i]).setValue (colourValues[i]); | |||
} | |||
std::unique_ptr<FileChooser> chooser; | |||
JUCE_DECLARE_NON_COPYABLE (EditorPanel) | |||
}; | |||
@@ -73,13 +73,20 @@ public: | |||
addAndMakeVisible (createButton); | |||
createButton.onClick = [this] | |||
{ | |||
FileChooser fc ("Save PIP File", | |||
File::getSpecialLocation (File::SpecialLocationType::userDesktopDirectory) | |||
.getChildFile (nameValue.get().toString() + ".h")); | |||
fc.browseForFileToSave (true); | |||
chooser = std::make_unique<FileChooser> ("Save PIP File", | |||
File::getSpecialLocation (File::SpecialLocationType::userDesktopDirectory) | |||
.getChildFile (nameValue.get().toString() + ".h")); | |||
auto flags = FileBrowserComponent::saveMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::warnAboutOverwriting; | |||
chooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
const auto result = fc.getResult(); | |||
createPIPFile (fc.getResult()); | |||
if (result != File{}) | |||
createPIPFile (result); | |||
}); | |||
}; | |||
pipTree.addListener (this); | |||
@@ -333,6 +340,8 @@ private: | |||
TextButton createButton { "Create PIP" }; | |||
std::unique_ptr<FileChooser> chooser; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PIPCreatorWindowComponent) | |||
}; |
@@ -33,9 +33,9 @@ class TranslationToolComponent : public Component | |||
public: | |||
TranslationToolComponent() | |||
: editorOriginal (documentOriginal, nullptr), | |||
editorPre (documentPre, nullptr), | |||
editorPost (documentPost, nullptr), | |||
editorResult (documentResult, nullptr) | |||
editorPre (documentPre, nullptr), | |||
editorPost (documentPost, nullptr), | |||
editorResult (documentResult, nullptr) | |||
{ | |||
instructionsLabel.setText ( | |||
"This utility converts translation files to/from a format that can be passed to automatic translation tools." | |||
@@ -114,17 +114,7 @@ public: | |||
} | |||
private: | |||
CodeDocument documentOriginal, documentPre, documentPost, documentResult; | |||
CodeEditorComponent editorOriginal, editorPre, editorPost, editorResult; | |||
Label label1, label2, label3, label4; | |||
Label instructionsLabel; | |||
TextButton generateButton { TRANS("Generate") }; | |||
TextButton scanProjectButton { "Scan project for TRANS macros" }; | |||
TextButton scanFolderButton { "Scan folder for TRANS macros" }; | |||
TextButton loadTranslationButton { "Load existing translation file..."}; | |||
//============================================================================== | |||
void generate() | |||
{ | |||
StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent())); | |||
@@ -154,28 +144,35 @@ private: | |||
void scanFolder() | |||
{ | |||
FileChooser fc ("Choose the root folder to search for the TRANS macros", | |||
File(), "*"); | |||
chooser = std::make_unique<FileChooser> ("Choose the root folder to search for the TRANS macros", | |||
File(), "*"); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (fc.browseForDirectory()) | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
StringArray strings; | |||
TranslationHelpers::scanFolderForTranslations (strings, fc.getResult()); | |||
setPreTranslationText (TranslationHelpers::mungeStrings(strings)); | |||
} | |||
}); | |||
} | |||
void loadFile() | |||
{ | |||
FileChooser fc ("Choose a translation file to load", | |||
File(), "*"); | |||
chooser = std::make_unique<FileChooser> ("Choose a translation file to load", File(), "*"); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; | |||
if (fc.browseForFileToOpen()) | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
const LocalisedStrings loadedStrings (fc.getResult(), false); | |||
documentOriginal.replaceAllContent (fc.getResult().loadFileAsString().trim()); | |||
setPreTranslationText (TranslationHelpers::getPreTranslationText (loadedStrings)); | |||
} | |||
}); | |||
} | |||
void setPreTranslationText (const String& text) | |||
@@ -184,4 +181,18 @@ private: | |||
editorPre.grabKeyboardFocus(); | |||
editorPre.selectAll(); | |||
} | |||
//============================================================================== | |||
CodeDocument documentOriginal, documentPre, documentPost, documentResult; | |||
CodeEditorComponent editorOriginal, editorPre, editorPost, editorResult; | |||
Label label1, label2, label3, label4; | |||
Label instructionsLabel; | |||
TextButton generateButton { TRANS("Generate") }, | |||
scanProjectButton { "Scan project for TRANS macros" }, | |||
scanFolderButton { "Scan folder for TRANS macros" }, | |||
loadTranslationButton { "Load existing translation file..."}; | |||
std::unique_ptr<FileChooser> chooser; | |||
}; |
@@ -223,8 +223,11 @@ void ProjucerApplication::systemRequestedQuit() | |||
} | |||
else | |||
{ | |||
if (closeAllMainWindows()) | |||
quit(); | |||
closeAllMainWindows ([] (bool closedSuccessfully) | |||
{ | |||
if (closedSuccessfully) | |||
ProjucerApplication::quit(); | |||
}); | |||
} | |||
} | |||
@@ -251,7 +254,7 @@ void ProjucerApplication::anotherInstanceStarted (const String& commandLine) | |||
ArgumentList list ({}, commandLine); | |||
for (auto& arg : list.arguments) | |||
openFile (arg.resolveAsFile()); | |||
openFile (arg.resolveAsFile(), nullptr); | |||
} | |||
} | |||
@@ -651,7 +654,7 @@ void ProjucerApplication::findAndLaunchExample (int selectedIndex) | |||
// example doesn't exist? | |||
jassert (example != File()); | |||
openFile (example); | |||
openFile (example, nullptr); | |||
} | |||
//============================================================================== | |||
@@ -863,7 +866,7 @@ void ProjucerApplication::handleMainMenuCommand (int menuItemID) | |||
if (menuItemID >= recentProjectsBaseID && menuItemID < (recentProjectsBaseID + 100)) | |||
{ | |||
// open a file from the "recent files" menu | |||
openFile (settings->recentFiles.getFile (menuItemID - recentProjectsBaseID)); | |||
openFile (settings->recentFiles.getFile (menuItemID - recentProjectsBaseID), nullptr); | |||
} | |||
else if (menuItemID >= openWindowsBaseID && menuItemID < (openWindowsBaseID + 100)) | |||
{ | |||
@@ -1095,23 +1098,33 @@ void ProjucerApplication::createNewProjectFromClipboard() | |||
tempFile.create(); | |||
tempFile.appendText (SystemClipboard::getTextFromClipboard()); | |||
String errorString; | |||
auto cleanup = [tempFile] (String errorString) | |||
{ | |||
if (errorString.isNotEmpty()) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Error", errorString); | |||
tempFile.deleteFile(); | |||
} | |||
}; | |||
if (! isPIPFile (tempFile)) | |||
{ | |||
errorString = "Clipboard does not contain a valid PIP."; | |||
} | |||
else if (! openFile (tempFile)) | |||
{ | |||
errorString = "Couldn't create project from clipboard contents."; | |||
mainWindowList.closeWindow (mainWindowList.windows.getLast()); | |||
cleanup ("Clipboard does not contain a valid PIP."); | |||
return; | |||
} | |||
if (errorString.isNotEmpty()) | |||
WeakReference<ProjucerApplication> parent { this }; | |||
openFile (tempFile, [parent, cleanup] (bool openedSuccessfully) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Error", errorString); | |||
tempFile.deleteFile(); | |||
} | |||
if (parent == nullptr) | |||
return; | |||
if (! openedSuccessfully) | |||
{ | |||
cleanup ("Couldn't create project from clipboard contents."); | |||
parent->mainWindowList.closeWindow (parent->mainWindowList.windows.getLast()); | |||
} | |||
}); | |||
} | |||
void ProjucerApplication::createNewPIP() | |||
@@ -1121,45 +1134,57 @@ void ProjucerApplication::createNewPIP() | |||
void ProjucerApplication::askUserToOpenFile() | |||
{ | |||
FileChooser fc ("Open File"); | |||
chooser = std::make_unique<FileChooser> ("Open File"); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; | |||
chooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
const auto result = fc.getResult(); | |||
if (fc.browseForFileToOpen()) | |||
openFile (fc.getResult()); | |||
if (result != File{}) | |||
openFile (result, nullptr); | |||
}); | |||
} | |||
bool ProjucerApplication::openFile (const File& file) | |||
void ProjucerApplication::openFile (const File& file, std::function<void (bool)> callback) | |||
{ | |||
return mainWindowList.openFile (file); | |||
mainWindowList.openFile (file, std::move (callback)); | |||
} | |||
void ProjucerApplication::saveAllDocuments() | |||
{ | |||
openDocumentManager.saveAll(); | |||
openDocumentManager.saveAllSyncWithoutAsking(); | |||
for (int i = 0; i < mainWindowList.windows.size(); ++i) | |||
if (auto* pcc = mainWindowList.windows.getUnchecked(i)->getProjectContentComponent()) | |||
pcc->refreshProjectTreeFileStatuses(); | |||
} | |||
bool ProjucerApplication::closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave) | |||
void ProjucerApplication::closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave) | |||
{ | |||
return openDocumentManager.closeAll (askUserToSave); | |||
openDocumentManager.closeAllAsync (askUserToSave, nullptr); | |||
} | |||
bool ProjucerApplication::closeAllMainWindows() | |||
void ProjucerApplication::closeAllMainWindows (std::function<void (bool)> callback) | |||
{ | |||
return mainWindowList.askAllWindowsToClose(); | |||
mainWindowList.askAllWindowsToClose (std::move (callback)); | |||
} | |||
void ProjucerApplication::closeAllMainWindowsAndQuitIfNeeded() | |||
{ | |||
if (closeAllMainWindows()) | |||
WeakReference<ProjucerApplication> parent; | |||
closeAllMainWindows ([parent] (bool closedSuccessfully) | |||
{ | |||
#if ! JUCE_MAC | |||
if (mainWindowList.windows.size() == 0) | |||
systemRequestedQuit(); | |||
#if JUCE_MAC | |||
ignoreUnused (parent, closedSuccessfully); | |||
#else | |||
if (parent == nullptr) | |||
return; | |||
if (closedSuccessfully && parent->mainWindowList.windows.size() == 0) | |||
parent->systemRequestedQuit(); | |||
#endif | |||
} | |||
}); | |||
} | |||
void ProjucerApplication::clearRecentFiles() | |||
@@ -66,7 +66,7 @@ public: | |||
bool isGUIEditorEnabled() const; | |||
//============================================================================== | |||
bool openFile (const File&); | |||
void openFile (const File&, std::function<void (bool)>); | |||
void showPathsWindow (bool highlightJUCEPath = false); | |||
PropertiesFile::Options getPropertyFileOptionsFor (const String& filename, bool isProjectSettings); | |||
void selectEditorColourSchemeWithName (const String& schemeName); | |||
@@ -119,8 +119,8 @@ private: | |||
void createNewPIP(); | |||
void askUserToOpenFile(); | |||
void saveAllDocuments(); | |||
bool closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave); | |||
bool closeAllMainWindows(); | |||
void closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave); | |||
void closeAllMainWindows (std::function<void (bool)>); | |||
void closeAllMainWindowsAndQuitIfNeeded(); | |||
void clearRecentFiles(); | |||
@@ -216,6 +216,9 @@ private: | |||
int selectedColourSchemeIndex = 0, selectedEditorColourSchemeIndex = 0; | |||
std::unique_ptr<FileChooser> chooser; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjucerApplication) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (ProjucerApplication) | |||
}; |
@@ -238,12 +238,17 @@ private: | |||
void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const VersionInfo::Asset& asset) | |||
{ | |||
FileChooser chooser ("Please select the location into which you would like to install the new version", | |||
{ getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get() }); | |||
chooser = std::make_unique<FileChooser> ("Please select the location into which you would like to install the new version", | |||
File { getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get() }); | |||
auto flags = FileBrowserComponent::openMode | |||
| FileBrowserComponent::canSelectDirectories; | |||
if (chooser.browseForDirectory()) | |||
chooser->launchAsync (flags, [this, asset] (const FileChooser& fc) | |||
{ | |||
auto targetFolder = chooser.getResult(); | |||
auto targetFolder = fc.getResult(); | |||
if (targetFolder == File{}) | |||
return; | |||
// By default we will install into 'targetFolder/JUCE', but we should install into | |||
// 'targetFolder' if that is an existing JUCE directory. | |||
@@ -259,6 +264,15 @@ void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const Version | |||
auto targetFolderPath = targetFolder.getFullPathName(); | |||
WeakReference<LatestVersionCheckerAndUpdater> parent { this }; | |||
auto callback = ModalCallbackFunction::create ([parent, asset, targetFolder] (int result) | |||
{ | |||
if (parent == nullptr || result == 0) | |||
return; | |||
parent->downloadAndInstall (asset, targetFolder); | |||
}); | |||
if (willOverwriteJuceFolder) | |||
{ | |||
if (targetFolder.getChildFile (".git").isDirectory()) | |||
@@ -269,25 +283,32 @@ void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const Version | |||
return; | |||
} | |||
if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Overwrite Existing JUCE Folder?", | |||
"Do you want to replace the folder\n\n" + targetFolderPath + "\n\nwith the latest version from juce.com?\n\n" | |||
"This will move the existing folder to " + targetFolderPath + "_old.\n\n" | |||
"Replacing the folder that contains the currently running Projucer executable may not work on Windows.")) | |||
{ | |||
return; | |||
} | |||
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
"Overwrite Existing JUCE Folder?", | |||
"Do you want to replace the folder\n\n" + targetFolderPath + "\n\nwith the latest version from juce.com?\n\n" | |||
"This will move the existing folder to " + targetFolderPath + "_old.\n\n" | |||
"Replacing the folder that contains the currently running Projucer executable may not work on Windows.", | |||
{}, | |||
{}, | |||
nullptr, | |||
callback); | |||
return; | |||
} | |||
else if (targetFolder.exists()) | |||
if (targetFolder.exists()) | |||
{ | |||
if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Existing File Or Directory", | |||
"Do you want to move\n\n" + targetFolderPath + "\n\nto\n\n" + targetFolderPath + "_old?")) | |||
{ | |||
return; | |||
} | |||
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
"Existing File Or Directory", | |||
"Do you want to move\n\n" + targetFolderPath + "\n\nto\n\n" + targetFolderPath + "_old?", | |||
{}, | |||
{}, | |||
nullptr, | |||
callback); | |||
return; | |||
} | |||
downloadAndInstall (asset, targetFolder); | |||
} | |||
}); | |||
} | |||
void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString, | |||
@@ -56,4 +56,7 @@ private: | |||
std::unique_ptr<DownloadAndInstallThread> installer; | |||
std::unique_ptr<Component> dialogWindow; | |||
std::unique_ptr<FileChooser> chooser; | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (LatestVersionCheckerAndUpdater) | |||
}; |
@@ -100,13 +100,18 @@ namespace | |||
if (fixMissingDependencies) | |||
tryToFixMissingModuleDependencies(); | |||
auto error = justSaveResources ? project->saveResourcesOnly() | |||
: project->saveProject(); | |||
const auto onCompletion = [this] (Result result) | |||
{ | |||
project.reset(); | |||
project.reset(); | |||
if (result.failed()) | |||
ConsoleApplication::fail ("Error when saving: " + result.getErrorMessage()); | |||
}; | |||
if (error.failed()) | |||
ConsoleApplication::fail ("Error when saving: " + error.getErrorMessage()); | |||
if (justSaveResources) | |||
onCompletion (project->saveResourcesOnly()); | |||
else | |||
project->saveProject (Async::no, nullptr, onCompletion); | |||
} | |||
} | |||
@@ -229,10 +229,15 @@ void MainWindow::closeButtonPressed() | |||
ProjucerApplication::getApp().mainWindowList.closeWindow (this); | |||
} | |||
bool MainWindow::closeCurrentProject (OpenDocumentManager::SaveIfNeeded askUserToSave) | |||
void MainWindow::closeCurrentProject (OpenDocumentManager::SaveIfNeeded askUserToSave, std::function<void (bool)> callback) | |||
{ | |||
if (currentProject == nullptr) | |||
return true; | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
currentProject->getStoredProperties().setValue (getProjectWindowPosName(), getWindowStateAsString()); | |||
@@ -242,27 +247,65 @@ bool MainWindow::closeCurrentProject (OpenDocumentManager::SaveIfNeeded askUserT | |||
pcc->hideEditor(); | |||
} | |||
if (ProjucerApplication::getApp().openDocumentManager | |||
.closeAllDocumentsUsingProject (*currentProject, askUserToSave)) | |||
SafePointer<MainWindow> parent { this }; | |||
ProjucerApplication::getApp().openDocumentManager | |||
.closeAllDocumentsUsingProjectAsync (*currentProject, askUserToSave, [parent, askUserToSave, callback] (bool closedSuccessfully) | |||
{ | |||
if (askUserToSave == OpenDocumentManager::SaveIfNeeded::no | |||
|| (currentProject->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)) | |||
if (parent == nullptr) | |||
return; | |||
if (! closedSuccessfully) | |||
{ | |||
setProject (nullptr); | |||
return true; | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
} | |||
return false; | |||
auto setProjectAndCallback = [parent, callback] | |||
{ | |||
parent->setProject (nullptr); | |||
if (callback != nullptr) | |||
callback (true); | |||
}; | |||
if (askUserToSave == OpenDocumentManager::SaveIfNeeded::no) | |||
{ | |||
setProjectAndCallback(); | |||
return; | |||
} | |||
parent->currentProject->saveIfNeededAndUserAgreesAsync ([parent, setProjectAndCallback, callback] (FileBasedDocument::SaveResult saveResult) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (saveResult == FileBasedDocument::savedOk) | |||
setProjectAndCallback(); | |||
else if (callback != nullptr) | |||
callback (false); | |||
}); | |||
}); | |||
} | |||
void MainWindow::moveProject (File newProjectFileToOpen, OpenInIDE openInIDE) | |||
{ | |||
closeCurrentProject (OpenDocumentManager::SaveIfNeeded::no); | |||
openFile (newProjectFileToOpen); | |||
SafePointer<MainWindow> parent { this }; | |||
closeCurrentProject (OpenDocumentManager::SaveIfNeeded::no, [parent, newProjectFileToOpen, openInIDE] (bool) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
parent->openFile (newProjectFileToOpen, [parent, openInIDE] (bool openedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (currentProject != nullptr && openInIDE == OpenInIDE::yes) | |||
ProjucerApplication::getApp().getCommandManager().invokeDirectly (CommandIDs::openInIDE, false); | |||
if (openedSuccessfully && parent->currentProject != nullptr && openInIDE == OpenInIDE::yes) | |||
ProjucerApplication::getApp().getCommandManager().invokeDirectly (CommandIDs::openInIDE, false); | |||
}); | |||
}); | |||
} | |||
void MainWindow::setProject (std::unique_ptr<Project> newProject) | |||
@@ -308,44 +351,102 @@ bool MainWindow::canOpenFile (const File& file) const | |||
|| ProjucerApplication::getApp().openDocumentManager.canOpenFile (file)); | |||
} | |||
bool MainWindow::openFile (const File& file) | |||
void MainWindow::openFile (const File& file, std::function<void (bool)> callback) | |||
{ | |||
if (file.hasFileExtension (Project::projectFileExtension)) | |||
{ | |||
auto newDoc = std::make_unique<Project> (file); | |||
auto result = newDoc->loadFrom (file, true); | |||
if (result.wasOk() && closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes)) | |||
if (result.wasOk()) | |||
{ | |||
setProject (std::move (newDoc)); | |||
currentProject->setChangedFlag (false); | |||
SafePointer<MainWindow> parent { this }; | |||
auto sharedDoc = std::make_shared<std::unique_ptr<Project>> (std::move (newDoc)); | |||
closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, [parent, sharedDoc, callback] (bool saveResult) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (saveResult) | |||
{ | |||
parent->setProject (std::move (*sharedDoc.get())); | |||
parent->currentProject->setChangedFlag (false); | |||
parent->createProjectContentCompIfNeeded(); | |||
parent->getProjectContentComponent()->reloadLastOpenDocuments(); | |||
createProjectContentCompIfNeeded(); | |||
getProjectContentComponent()->reloadLastOpenDocuments(); | |||
parent->currentProject->updateDeprecatedProjectSettingsInteractively(); | |||
} | |||
currentProject->updateDeprecatedProjectSettingsInteractively(); | |||
if (callback != nullptr) | |||
callback (saveResult); | |||
}); | |||
return true; | |||
return; | |||
} | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
else if (file.exists()) | |||
if (file.exists()) | |||
{ | |||
if (isPIPFile (file) && openPIP ({ file })) | |||
return true; | |||
SafePointer<MainWindow> parent { this }; | |||
auto createCompAndShowEditor = [parent, file, callback] | |||
{ | |||
if (parent != nullptr) | |||
{ | |||
parent->createProjectContentCompIfNeeded(); | |||
createProjectContentCompIfNeeded(); | |||
return getProjectContentComponent()->showEditorForFile (file, true); | |||
if (callback != nullptr) | |||
callback (parent->getProjectContentComponent()->showEditorForFile (file, true)); | |||
} | |||
}; | |||
if (isPIPFile (file)) | |||
{ | |||
openPIP (file, [parent, createCompAndShowEditor, callback] (bool openedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (openedSuccessfully) | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
createCompAndShowEditor(); | |||
}); | |||
return; | |||
} | |||
createCompAndShowEditor(); | |||
return; | |||
} | |||
return false; | |||
if (callback != nullptr) | |||
callback (false); | |||
} | |||
bool MainWindow::openPIP (PIPGenerator generator) | |||
void MainWindow::openPIP (const File& pipFile, std::function<void (bool)> callback) | |||
{ | |||
if (! generator.hasValidPIP()) | |||
return false; | |||
auto generator = std::make_shared<PIPGenerator> (pipFile); | |||
auto generatorResult = generator.createJucerFile(); | |||
if (! generator->hasValidPIP()) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
auto generatorResult = generator->createJucerFile(); | |||
if (generatorResult != Result::ok()) | |||
{ | |||
@@ -353,29 +454,47 @@ bool MainWindow::openPIP (PIPGenerator generator) | |||
"PIP Error.", | |||
generatorResult.getErrorMessage()); | |||
return false; | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
if (! generator.createMainCpp()) | |||
if (! generator->createMainCpp()) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"PIP Error.", | |||
"Failed to create Main.cpp."); | |||
return false; | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
if (! openFile (generator.getJucerFile())) | |||
SafePointer<MainWindow> parent { this }; | |||
openFile (generator->getJucerFile(), [parent, generator, callback] (bool openedSuccessfully) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"PIP Error.", | |||
"Failed to open .jucer file."); | |||
if (parent == nullptr) | |||
return; | |||
return false; | |||
} | |||
if (! openedSuccessfully) | |||
{ | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"PIP Error.", | |||
"Failed to open .jucer file."); | |||
setupTemporaryPIPProject (generator); | |||
return true; | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
parent->setupTemporaryPIPProject (*generator); | |||
if (callback != nullptr) | |||
callback (true); | |||
}); | |||
} | |||
void MainWindow::setupTemporaryPIPProject (PIPGenerator& generator) | |||
@@ -408,15 +527,32 @@ bool MainWindow::isInterestedInFileDrag (const StringArray& filenames) | |||
return false; | |||
} | |||
void MainWindow::filesDropped (const StringArray& filenames, int /*mouseX*/, int /*mouseY*/) | |||
static void filesDroppedRecursive (Component::SafePointer<MainWindow> parent, StringArray filenames) | |||
{ | |||
for (auto& filename : filenames) | |||
{ | |||
const File f (filename); | |||
if (filenames.isEmpty()) | |||
return; | |||
if (canOpenFile (f) && openFile (f)) | |||
break; | |||
auto f = filenames[0]; | |||
filenames.remove (0); | |||
if (! parent->canOpenFile (f)) | |||
{ | |||
filesDroppedRecursive (parent, filenames); | |||
return; | |||
} | |||
parent->openFile (f, [parent, filenames] (bool openedSuccessfully) | |||
{ | |||
if (parent == nullptr || ! openedSuccessfully) | |||
return; | |||
filesDroppedRecursive (parent, filenames); | |||
}); | |||
} | |||
void MainWindow::filesDropped (const StringArray& filenames, int /*mouseX*/, int /*mouseY*/) | |||
{ | |||
filesDroppedRecursive (this, filenames); | |||
} | |||
bool MainWindow::shouldDropFilesWhenDraggedExternally (const DragAndDropTarget::SourceDetails& sourceDetails, | |||
@@ -472,7 +608,7 @@ void MainWindow::showStartPage() | |||
jassert (currentProject == nullptr); | |||
setContentOwned (new StartPageComponent ([this] (std::unique_ptr<Project>&& newProject) { setProject (std::move (newProject)); }, | |||
[this] (const File& exampleFile) { openFile (exampleFile); }), | |||
[this] (const File& exampleFile) { openFile (exampleFile, nullptr); }), | |||
true); | |||
setResizable (false, false); | |||
@@ -580,19 +716,38 @@ void MainWindowList::forceCloseAllWindows() | |||
windows.clear(); | |||
} | |||
bool MainWindowList::askAllWindowsToClose() | |||
static void askAllWindowsToCloseRecursive (WeakReference<MainWindowList> parent, std::function<void (bool)> callback) | |||
{ | |||
saveCurrentlyOpenProjectList(); | |||
while (windows.size() > 0) | |||
if (parent->windows.size() == 0) | |||
{ | |||
if (! windows[0]->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes)) | |||
return false; | |||
if (callback != nullptr) | |||
callback (true); | |||
windows.remove (0); | |||
return; | |||
} | |||
return true; | |||
parent->windows[0]->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, [parent, callback] (bool closedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! closedSuccessfully) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
parent->windows.remove (0); | |||
askAllWindowsToCloseRecursive (parent, std::move (callback)); | |||
}); | |||
} | |||
void MainWindowList::askAllWindowsToClose (std::function<void (bool)> callback) | |||
{ | |||
saveCurrentlyOpenProjectList(); | |||
askAllWindowsToCloseRecursive (this, std::move (callback)); | |||
} | |||
void MainWindowList::createWindowIfNoneAreOpen() | |||
@@ -613,11 +768,18 @@ void MainWindowList::closeWindow (MainWindow* w) | |||
else | |||
#endif | |||
{ | |||
if (w->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes)) | |||
WeakReference<MainWindowList> parent { this }; | |||
w->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, [parent, w] (bool closedSuccessfully) | |||
{ | |||
windows.removeObject (w); | |||
saveCurrentlyOpenProjectList(); | |||
} | |||
if (parent == nullptr) | |||
return; | |||
if (closedSuccessfully) | |||
{ | |||
parent->windows.removeObject (w); | |||
parent->saveCurrentlyOpenProjectList(); | |||
} | |||
}); | |||
} | |||
} | |||
@@ -653,20 +815,31 @@ void MainWindowList::openDocument (OpenDocumentManager::Document* doc, bool grab | |||
getFrontmostWindow()->getProjectContentComponent()->showDocument (doc, grabFocus); | |||
} | |||
bool MainWindowList::openFile (const File& file, bool openInBackground) | |||
void MainWindowList::openFile (const File& file, std::function<void (bool)> callback, bool openInBackground) | |||
{ | |||
if (! file.exists()) | |||
return false; | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
for (auto* w : windows) | |||
{ | |||
if (w->getProject() != nullptr && w->getProject()->getFile() == file) | |||
{ | |||
w->toFront (true); | |||
return true; | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
} | |||
WeakReference<MainWindowList> parent { this }; | |||
if (file.hasFileExtension (Project::projectFileExtension) | |||
|| isPIPFile (file)) | |||
{ | |||
@@ -675,23 +848,37 @@ bool MainWindowList::openFile (const File& file, bool openInBackground) | |||
auto* w = getOrCreateEmptyWindow(); | |||
jassert (w != nullptr); | |||
if (w->openFile (file)) | |||
w->openFile (file, [parent, previousFrontWindow, w, openInBackground, callback] (bool openedSuccessfully) | |||
{ | |||
w->makeVisible(); | |||
w->setResizable (true, false); | |||
checkWindowBounds (*w); | |||
if (parent == nullptr) | |||
return; | |||
if (openInBackground && previousFrontWindow != nullptr) | |||
previousFrontWindow->toFront (true); | |||
if (openedSuccessfully) | |||
{ | |||
w->makeVisible(); | |||
w->setResizable (true, false); | |||
parent->checkWindowBounds (*w); | |||
return true; | |||
} | |||
if (openInBackground && previousFrontWindow != nullptr) | |||
previousFrontWindow->toFront (true); | |||
} | |||
else | |||
{ | |||
parent->closeWindow (w); | |||
} | |||
if (callback != nullptr) | |||
callback (openedSuccessfully); | |||
}); | |||
closeWindow (w); | |||
return false; | |||
return; | |||
} | |||
return getFrontmostWindow()->openFile (file); | |||
getFrontmostWindow()->openFile (file, [parent, callback] (bool openedSuccessfully) | |||
{ | |||
if (parent != nullptr && callback != nullptr) | |||
callback (openedSuccessfully); | |||
}); | |||
} | |||
MainWindow* MainWindowList::createNewMainWindow() | |||
@@ -841,7 +1028,7 @@ void MainWindowList::reopenLastProjects() | |||
for (auto& p : getAppSettings().getLastProjects()) | |||
if (p.existsAsFile()) | |||
openFile (p, true); | |||
openFile (p, nullptr, true); | |||
} | |||
void MainWindowList::sendLookAndFeelChange() | |||
@@ -53,7 +53,7 @@ public: | |||
//============================================================================== | |||
bool canOpenFile (const File& file) const; | |||
bool openFile (const File& file); | |||
void openFile (const File& file, std::function<void (bool)> callback); | |||
void setProject (std::unique_ptr<Project> newProject); | |||
Project* getProject() const { return currentProject.get(); } | |||
@@ -61,7 +61,7 @@ public: | |||
void makeVisible(); | |||
void restoreWindowPosition(); | |||
void updateTitleBarIcon(); | |||
bool closeCurrentProject (OpenDocumentManager::SaveIfNeeded askToSave); | |||
void closeCurrentProject (OpenDocumentManager::SaveIfNeeded askToSave, std::function<void (bool)> callback); | |||
void moveProject (File newProjectFile, OpenInIDE openInIDE); | |||
void showStartPage(); | |||
@@ -91,7 +91,7 @@ private: | |||
static const char* getProjectWindowPosName() { return "projectWindowPos"; } | |||
void createProjectContentCompIfNeeded(); | |||
bool openPIP (PIPGenerator); | |||
void openPIP (const File&, std::function<void (bool)> callback); | |||
void setupTemporaryPIPProject (PIPGenerator&); | |||
void initialiseProjectWindow(); | |||
@@ -112,14 +112,14 @@ public: | |||
MainWindowList(); | |||
void forceCloseAllWindows(); | |||
bool askAllWindowsToClose(); | |||
void askAllWindowsToClose (std::function<void (bool)> callback); | |||
void closeWindow (MainWindow*); | |||
void goToSiblingWindow (MainWindow*, int delta); | |||
void createWindowIfNoneAreOpen(); | |||
void openDocument (OpenDocumentManager::Document*, bool grabFocus); | |||
bool openFile (const File& file, bool openInBackground = false); | |||
void openFile (const File& file, std::function<void (bool)> callback, bool openInBackground = false); | |||
MainWindow* createNewMainWindow(); | |||
MainWindow* getFrontmostWindow (bool createIfNotFound = true); | |||
@@ -142,4 +142,5 @@ private: | |||
bool isInReopenLastProjects = false; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindowList) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (MainWindowList) | |||
}; |
@@ -53,8 +53,9 @@ public: | |||
bool refersToProject (Project& p) const override { return project == &p; } | |||
Project* getProject() const override { return project; } | |||
bool needsSaving() const override { return false; } | |||
bool save() override { return true; } | |||
bool saveAs() override { return false; } | |||
bool saveSyncWithoutAsking() override { return true; } | |||
void saveAsync (std::function<void (bool)>) override {} | |||
void saveAsAsync (std::function<void (bool)>) override {} | |||
bool hasFileBeenModifiedExternally() override { return fileModificationTime != file.getLastModificationTime(); } | |||
void reloadFromFile() override { fileModificationTime = file.getLastModificationTime(); } | |||
String getName() const override { return file.getFileName(); } | |||
@@ -164,86 +165,201 @@ OpenDocumentManager::Document* OpenDocumentManager::getOpenDocument (int index) | |||
return documents.getUnchecked (index); | |||
} | |||
FileBasedDocument::SaveResult OpenDocumentManager::saveIfNeededAndUserAgrees (OpenDocumentManager::Document* doc) | |||
void OpenDocumentManager::saveIfNeededAndUserAgrees (OpenDocumentManager::Document* doc, | |||
std::function<void (FileBasedDocument::SaveResult)> callback) | |||
{ | |||
if (! doc->needsSaving()) | |||
return FileBasedDocument::savedOk; | |||
{ | |||
if (callback != nullptr) | |||
callback (FileBasedDocument::savedOk); | |||
const int r = AlertWindow::showYesNoCancelBox (AlertWindow::QuestionIcon, | |||
TRANS("Closing document..."), | |||
TRANS("Do you want to save the changes to \"") | |||
+ doc->getName() + "\"?", | |||
TRANS("Save"), | |||
TRANS("Discard changes"), | |||
TRANS("Cancel")); | |||
return; | |||
} | |||
if (r == 1) // save changes | |||
return doc->save() ? FileBasedDocument::savedOk | |||
: FileBasedDocument::failedToWriteToFile; | |||
WeakReference<OpenDocumentManager> parent { this }; | |||
AlertWindow::showYesNoCancelBox (AlertWindow::QuestionIcon, | |||
TRANS("Closing document..."), | |||
TRANS("Do you want to save the changes to \"") | |||
+ doc->getName() + "\"?", | |||
TRANS("Save"), | |||
TRANS("Discard changes"), | |||
TRANS("Cancel"), | |||
nullptr, | |||
ModalCallbackFunction::create ([parent, doc, callback] (int r) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (r == 2) // discard changes | |||
return FileBasedDocument::savedOk; | |||
if (r == 1) | |||
{ | |||
doc->saveAsync ([parent, callback] (bool hasSaved) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
return FileBasedDocument::userCancelledSave; | |||
} | |||
if (callback != nullptr) | |||
callback (hasSaved ? FileBasedDocument::savedOk : FileBasedDocument::failedToWriteToFile); | |||
}); | |||
return; | |||
} | |||
if (callback != nullptr) | |||
callback (r == 2 ? FileBasedDocument::savedOk : FileBasedDocument::userCancelledSave); | |||
})); | |||
} | |||
bool OpenDocumentManager::closeDocument (int index, SaveIfNeeded saveIfNeeded) | |||
bool OpenDocumentManager::closeDocumentWithoutSaving (Document* doc) | |||
{ | |||
if (Document* doc = documents [index]) | |||
if (documents.contains (doc)) | |||
{ | |||
if (saveIfNeeded == SaveIfNeeded::yes) | |||
if (saveIfNeededAndUserAgrees (doc) != FileBasedDocument::savedOk) | |||
return false; | |||
bool canClose = true; | |||
for (int i = listeners.size(); --i >= 0;) | |||
if (DocumentCloseListener* l = listeners[i]) | |||
if (auto* l = listeners[i]) | |||
if (! l->documentAboutToClose (doc)) | |||
canClose = false; | |||
if (! canClose) | |||
return false; | |||
documents.remove (index); | |||
documents.removeObject (doc); | |||
ProjucerApplication::getCommandManager().commandStatusChanged(); | |||
} | |||
return true; | |||
} | |||
bool OpenDocumentManager::closeDocument (Document* document, SaveIfNeeded saveIfNeeded) | |||
void OpenDocumentManager::closeDocumentAsync (Document* doc, SaveIfNeeded saveIfNeeded, std::function<void (bool)> callback) | |||
{ | |||
return closeDocument (documents.indexOf (document), saveIfNeeded); | |||
if (! documents.contains (doc)) | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
if (saveIfNeeded == SaveIfNeeded::yes) | |||
{ | |||
WeakReference<OpenDocumentManager> parent { this }; | |||
saveIfNeededAndUserAgrees (doc, [parent, doc, callback] (FileBasedDocument::SaveResult result) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (result != FileBasedDocument::savedOk) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
if (callback != nullptr) | |||
callback (parent->closeDocumentWithoutSaving (doc)); | |||
}); | |||
return; | |||
} | |||
if (callback != nullptr) | |||
callback (closeDocumentWithoutSaving (doc)); | |||
} | |||
void OpenDocumentManager::closeFile (const File& f, SaveIfNeeded saveIfNeeded) | |||
void OpenDocumentManager::closeFileWithoutSaving (const File& f) | |||
{ | |||
for (int i = documents.size(); --i >= 0;) | |||
if (Document* d = documents[i]) | |||
if (auto* d = documents[i]) | |||
if (d->isForFile (f)) | |||
closeDocument (i, saveIfNeeded); | |||
closeDocumentWithoutSaving (d); | |||
} | |||
bool OpenDocumentManager::closeAll (SaveIfNeeded askUserToSave) | |||
static void closeLastAsyncRecusrsive (WeakReference<OpenDocumentManager> parent, | |||
OpenDocumentManager::SaveIfNeeded askUserToSave, | |||
std::function<void (bool)> callback) | |||
{ | |||
for (int i = getNumOpenDocuments(); --i >= 0;) | |||
if (! closeDocument (i, askUserToSave)) | |||
return false; | |||
auto lastIndex = parent->getNumOpenDocuments() - 1; | |||
return true; | |||
if (lastIndex < 0) | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
parent->closeDocumentAsync (parent->getOpenDocument (lastIndex), | |||
askUserToSave, | |||
[parent, askUserToSave, callback] (bool closedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! closedSuccessfully) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
closeLastAsyncRecusrsive (parent, askUserToSave, std::move (callback)); | |||
}); | |||
} | |||
void OpenDocumentManager::closeAllAsync (SaveIfNeeded askUserToSave, std::function<void (bool)> callback) | |||
{ | |||
closeLastAsyncRecusrsive (this, askUserToSave, std::move (callback)); | |||
} | |||
void OpenDocumentManager::closeLastDocumentUsingProjectRecursive (WeakReference<OpenDocumentManager> parent, | |||
Project* project, | |||
SaveIfNeeded askUserToSave, | |||
std::function<void (bool)> callback) | |||
{ | |||
for (int i = documents.size(); --i >= 0;) | |||
{ | |||
if (auto* d = documents[i]) | |||
{ | |||
if (d->getProject() == project) | |||
{ | |||
closeDocumentAsync (d, askUserToSave, [parent, project, askUserToSave, callback] (bool closedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! closedSuccessfully) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
parent->closeLastDocumentUsingProjectRecursive (parent, project, askUserToSave, std::move (callback)); | |||
}); | |||
return; | |||
} | |||
} | |||
} | |||
if (callback != nullptr) | |||
callback (true); | |||
} | |||
void OpenDocumentManager::closeAllDocumentsUsingProjectAsync (Project& project, SaveIfNeeded askUserToSave, std::function<void (bool)> callback) | |||
{ | |||
WeakReference<OpenDocumentManager> parent { this }; | |||
closeLastDocumentUsingProjectRecursive (parent, &project, askUserToSave, std::move (callback)); | |||
} | |||
bool OpenDocumentManager::closeAllDocumentsUsingProject (Project& project, SaveIfNeeded saveIfNeeded) | |||
void OpenDocumentManager::closeAllDocumentsUsingProjectWithoutSaving (Project& project) | |||
{ | |||
for (int i = documents.size(); --i >= 0;) | |||
if (Document* d = documents[i]) | |||
if (d->refersToProject (project)) | |||
if (! closeDocument (i, saveIfNeeded)) | |||
return false; | |||
return true; | |||
closeDocumentWithoutSaving (d); | |||
} | |||
bool OpenDocumentManager::anyFilesNeedSaving() const | |||
@@ -255,17 +371,13 @@ bool OpenDocumentManager::anyFilesNeedSaving() const | |||
return false; | |||
} | |||
bool OpenDocumentManager::saveAll() | |||
void OpenDocumentManager::saveAllSyncWithoutAsking() | |||
{ | |||
for (int i = documents.size(); --i >= 0;) | |||
{ | |||
if (! documents.getUnchecked (i)->save()) | |||
return false; | |||
ProjucerApplication::getCommandManager().commandStatusChanged(); | |||
if (documents.getUnchecked (i)->saveSyncWithoutAsking()) | |||
ProjucerApplication::getCommandManager().commandStatusChanged(); | |||
} | |||
return true; | |||
} | |||
void OpenDocumentManager::reloadModifiedFiles() | |||
@@ -51,8 +51,9 @@ public: | |||
virtual String getType() const = 0; | |||
virtual File getFile() const = 0; | |||
virtual bool needsSaving() const = 0; | |||
virtual bool save() = 0; | |||
virtual bool saveAs() = 0; | |||
virtual bool saveSyncWithoutAsking() = 0; | |||
virtual void saveAsync (std::function<void (bool)>) = 0; | |||
virtual void saveAsAsync (std::function<void (bool)>) = 0; | |||
virtual bool hasFileBeenModifiedExternally() = 0; | |||
virtual void reloadFromFile() = 0; | |||
virtual std::unique_ptr<Component> createEditor() = 0; | |||
@@ -72,14 +73,20 @@ public: | |||
bool canOpenFile (const File& file); | |||
Document* openFile (Project* project, const File& file); | |||
bool closeDocument (int index, SaveIfNeeded saveIfNeeded); | |||
bool closeDocument (Document* document, SaveIfNeeded saveIfNeeded); | |||
bool closeAll (SaveIfNeeded askUserToSave); | |||
bool closeAllDocumentsUsingProject (Project& project, SaveIfNeeded saveIfNeeded); | |||
void closeFile (const File& f, SaveIfNeeded saveIfNeeded); | |||
void closeDocumentAsync (Document* document, SaveIfNeeded saveIfNeeded, std::function<void (bool)> callback); | |||
bool closeDocumentWithoutSaving (Document* document); | |||
void closeAllAsync (SaveIfNeeded askUserToSave, std::function<void (bool)> callback); | |||
void closeAllDocumentsUsingProjectAsync (Project& project, SaveIfNeeded askUserToSave, std::function<void (bool)> callback); | |||
void closeAllDocumentsUsingProjectWithoutSaving (Project& project); | |||
void closeFileWithoutSaving (const File& f); | |||
bool anyFilesNeedSaving() const; | |||
bool saveAll(); | |||
FileBasedDocument::SaveResult saveIfNeededAndUserAgrees (Document* doc); | |||
void saveAllSyncWithoutAsking(); | |||
void saveIfNeededAndUserAgrees (Document* doc, std::function<void (FileBasedDocument::SaveResult)>); | |||
void reloadModifiedFiles(); | |||
void fileHasBeenRenamed (const File& oldFile, const File& newFile); | |||
@@ -112,11 +119,19 @@ public: | |||
private: | |||
//============================================================================== | |||
void closeLastDocumentUsingProjectRecursive (WeakReference<OpenDocumentManager>, | |||
Project*, | |||
SaveIfNeeded, | |||
std::function<void (bool)>); | |||
//============================================================================== | |||
OwnedArray<DocumentType> types; | |||
OwnedArray<Document> documents; | |||
Array<DocumentCloseListener*> listeners; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenDocumentManager) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (OpenDocumentManager) | |||
}; | |||
//============================================================================== | |||
@@ -98,7 +98,7 @@ static bool writeCodeDocToFile (const File& file, CodeDocument& doc) | |||
return temp.overwriteTargetFileWithTemporary(); | |||
} | |||
bool SourceCodeDocument::save() | |||
bool SourceCodeDocument::saveSyncWithoutAsking() | |||
{ | |||
if (writeCodeDocToFile (getFile(), getCodeDocument())) | |||
{ | |||
@@ -110,14 +110,28 @@ bool SourceCodeDocument::save() | |||
return false; | |||
} | |||
bool SourceCodeDocument::saveAs() | |||
void SourceCodeDocument::saveAsync (std::function<void (bool)> callback) | |||
{ | |||
FileChooser fc (TRANS("Save As..."), getFile(), "*"); | |||
callback (saveSyncWithoutAsking()); | |||
} | |||
if (! fc.browseForFileToSave (true)) | |||
return true; | |||
void SourceCodeDocument::saveAsAsync (std::function<void (bool)> callback) | |||
{ | |||
chooser = std::make_unique<FileChooser> (TRANS("Save As..."), getFile(), "*"); | |||
auto flags = FileBrowserComponent::saveMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::warnAboutOverwriting; | |||
chooser->launchAsync (flags, [this, callback] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
{ | |||
callback (true); | |||
return; | |||
} | |||
return writeCodeDocToFile (fc.getResult(), getCodeDocument()); | |||
callback (writeCodeDocToFile (fc.getResult(), getCodeDocument())); | |||
}); | |||
} | |||
void SourceCodeDocument::updateLastState (CodeEditorComponent& editor) | |||
@@ -642,18 +656,31 @@ void CppCodeEditorComponent::performPopupMenuAction (int menuItemID) | |||
void CppCodeEditorComponent::insertComponentClass() | |||
{ | |||
AlertWindow aw (TRANS ("Insert a new Component class"), | |||
TRANS ("Please enter a name for the new class"), | |||
AlertWindow::NoIcon, nullptr); | |||
asyncAlertWindow = std::make_unique<AlertWindow> (TRANS ("Insert a new Component class"), | |||
TRANS ("Please enter a name for the new class"), | |||
AlertWindow::NoIcon, | |||
nullptr); | |||
const char* classNameField = "Class Name"; | |||
const String classNameField { "Class Name" }; | |||
aw.addTextEditor (classNameField, String(), String(), false); | |||
aw.addButton (TRANS ("Insert Code"), 1, KeyPress (KeyPress::returnKey)); | |||
aw.addButton (TRANS ("Cancel"), 0, KeyPress (KeyPress::escapeKey)); | |||
asyncAlertWindow->addTextEditor (classNameField, String(), String(), false); | |||
asyncAlertWindow->addButton (TRANS ("Insert Code"), 1, KeyPress (KeyPress::returnKey)); | |||
asyncAlertWindow->addButton (TRANS ("Cancel"), 0, KeyPress (KeyPress::escapeKey)); | |||
while (aw.runModalLoop() != 0) | |||
SafePointer<CppCodeEditorComponent> parent { this }; | |||
asyncAlertWindow->enterModalState (true, ModalCallbackFunction::create ([parent, classNameField] (int result) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
auto& aw = *(parent->asyncAlertWindow); | |||
aw.exitModalState (result); | |||
aw.setVisible (false); | |||
if (result == 0) | |||
return; | |||
auto className = aw.getTextEditorContents (classNameField).trim(); | |||
if (className == build_tools::makeValidIdentifier (className, false, true, false)) | |||
@@ -661,8 +688,10 @@ void CppCodeEditorComponent::insertComponentClass() | |||
String code (BinaryData::jucer_InlineComponentTemplate_h); | |||
code = code.replace ("%%component_class%%", className); | |||
insertTextAtCaret (code); | |||
break; | |||
parent->insertTextAtCaret (code); | |||
return; | |||
} | |||
} | |||
parent->insertComponentClass(); | |||
})); | |||
} |
@@ -80,8 +80,9 @@ public: | |||
} | |||
void reloadFromFile() override; | |||
bool save() override; | |||
bool saveAs() override; | |||
bool saveSyncWithoutAsking() override; | |||
void saveAsync (std::function<void (bool)>) override; | |||
void saveAsAsync (std::function<void (bool)>) override; | |||
std::unique_ptr<Component> createEditor() override; | |||
std::unique_ptr<Component> createViewer() override { return createEditor(); } | |||
@@ -132,6 +133,9 @@ protected: | |||
std::unique_ptr<CodeEditorComponent::State> lastState; | |||
void reloadInternal(); | |||
private: | |||
std::unique_ptr<FileChooser> chooser; | |||
}; | |||
class GenericCodeEditorComponent; | |||
@@ -235,5 +239,7 @@ public: | |||
private: | |||
void insertComponentClass(); | |||
std::unique_ptr<AlertWindow> asyncAlertWindow; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CppCodeEditorComponent) | |||
}; |
@@ -140,7 +140,7 @@ protected: | |||
String colourIdCode, colourName, xmlTagName; | |||
}; | |||
OwnedArray <ComponentColourInfo> colours; | |||
OwnedArray<ComponentColourInfo> colours; | |||
private: | |||
JUCE_DECLARE_NON_COPYABLE (ComponentTypeHandler) | |||
@@ -672,13 +672,13 @@ private: | |||
m.addItem (i + 1, "Delete tab " + String (i) | |||
+ ": \"" + names[i] + "\""); | |||
const int r = m.showAt (this); | |||
if (r > 0) | |||
PopupMenu::Options options{}; | |||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (this), [this] (int r) | |||
{ | |||
document.perform (new RemoveTabAction (component, *document.getComponentLayout(), r - 1), | |||
"Remove a tab"); | |||
} | |||
if (r > 0) | |||
document.perform (new RemoveTabAction (component, *document.getComponentLayout(), r - 1), | |||
"Remove a tab"); | |||
}); | |||
} | |||
String getButtonText() const | |||
@@ -1131,11 +1131,13 @@ private: | |||
m.addItem (1, "Move this tab up", tabIndex > 0); | |||
m.addItem (2, "Move this tab down", tabIndex < totalNumTabs - 1); | |||
const int r = m.showAt (this); | |||
if (r != 0) | |||
document.perform (new MoveTabAction (component, *document.getComponentLayout(), tabIndex, tabIndex + (r == 2 ? 1 : -1)), | |||
"Move a tab"); | |||
PopupMenu::Options options{}; | |||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (this), [this] (int r) | |||
{ | |||
if (r != 0) | |||
document.perform (new MoveTabAction (component, *document.getComponentLayout(), tabIndex, tabIndex + (r == 2 ? 1 : -1)), | |||
"Move a tab"); | |||
}); | |||
} | |||
String getButtonText() const | |||
@@ -74,14 +74,16 @@ public: | |||
{ | |||
if (newIndex == 0) | |||
{ | |||
String resource (document.getResources() | |||
.browseForResource ("Select an image file to add as a resource", | |||
"*.jpg;*.jpeg;*.png;*.gif;*.svg", | |||
File(), | |||
String())); | |||
if (resource.isNotEmpty()) | |||
setResource (resource); | |||
document.getResources() | |||
.browseForResource ("Select an image file to add as a resource", | |||
"*.jpg;*.jpeg;*.png;*.gif;*.svg", | |||
File(), | |||
String(), | |||
[this] (String resource) | |||
{ | |||
if (resource.isNotEmpty()) | |||
setResource (resource); | |||
}); | |||
} | |||
else | |||
{ | |||
@@ -660,7 +660,6 @@ void PaintElement::updateSiblingComps() | |||
} | |||
} | |||
void PaintElement::showPopupMenu() | |||
{ | |||
auto* commandManager = &ProjucerApplication::getCommandManager(); | |||
@@ -685,5 +684,5 @@ void PaintElement::showPopupMenu() | |||
m.addCommandItem (commandManager, StandardApplicationCommandIDs::paste); | |||
m.addCommandItem (commandManager, StandardApplicationCommandIDs::del); | |||
m.show(); | |||
m.showMenuAsync ({}); | |||
} |
@@ -124,7 +124,7 @@ protected: | |||
void siblingComponentsChanged(); | |||
OwnedArray <ElementSiblingComponent> siblingComponents; | |||
OwnedArray<ElementSiblingComponent> siblingComponents; | |||
void updateSiblingComps(); | |||
@@ -127,7 +127,7 @@ public: | |||
private: | |||
friend class PathPoint; | |||
friend class PathPointComponent; | |||
OwnedArray <PathPoint> points; | |||
OwnedArray<PathPoint> points; | |||
bool nonZeroWinding; | |||
mutable Path path; | |||
mutable Rectangle<int> lastPathBounds; | |||
@@ -63,8 +63,15 @@ public: | |||
button.setConnectedEdges (TextButton::ConnectedOnLeft | TextButton::ConnectedOnRight); | |||
button.onClick = [this] | |||
{ | |||
if (showMenu (layout)) | |||
refresh(); // (to clear the text editor if it's got focus) | |||
SafePointer<PositionPropertyBase> safeThis { this }; | |||
showMenu (layout, [safeThis] (bool shouldRefresh) | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
if (shouldRefresh) | |||
safeThis->refresh(); // (to clear the text editor if it's got focus) | |||
}); | |||
}; | |||
textEditor.reset (new PositionPropLabel (*this)); | |||
@@ -173,7 +180,7 @@ public: | |||
refresh(); | |||
} | |||
bool showMenu (ComponentLayout* compLayout) | |||
void showMenu (ComponentLayout* compLayout, std::function<void (bool)> callback) | |||
{ | |||
RelativePositionedRectangle rpr (getPosition()); | |||
PositionedRectangle p (rpr.rect); | |||
@@ -255,127 +262,135 @@ public: | |||
m.addSubMenu ("Relative to", compLayout->getRelativeTargetMenu (component, (int) dimension)); | |||
} | |||
WeakReference<Component> ref (this); | |||
const int menuResult = m.showAt (&button); | |||
if (menuResult == 0 || ref == nullptr) | |||
return false; | |||
SafePointer<PositionPropertyBase> ref (this); | |||
switch (menuResult) | |||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (&button), | |||
[ref, compLayout, callback, xAnchor, yAnchor, xMode, yMode, sizeW, sizeH, p, rpr] (int menuResult) mutable | |||
{ | |||
case 10: | |||
if (dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentTopLeft; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentTopLeft; | |||
break; | |||
case 11: | |||
if (dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentBottomRight; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentBottomRight; | |||
break; | |||
case 12: | |||
if (dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentCentre; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentCentre; | |||
break; | |||
case 13: | |||
if (dimension == componentX) | |||
xMode = PositionedRectangle::proportionOfParentSize; | |||
else | |||
yMode = PositionedRectangle::proportionOfParentSize; | |||
break; | |||
case 14: | |||
if (dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtLeftOrTop; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtLeftOrTop; | |||
break; | |||
case 15: | |||
if (dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtCentre; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtCentre; | |||
break; | |||
case 16: | |||
if (dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtRightOrBottom; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtRightOrBottom; | |||
break; | |||
case 20: | |||
if (dimension == componentWidth) | |||
sizeW = PositionedRectangle::absoluteSize; | |||
else | |||
sizeH = PositionedRectangle::absoluteSize; | |||
break; | |||
case 21: | |||
if (dimension == componentWidth) | |||
sizeW = PositionedRectangle::proportionalSize; | |||
else | |||
sizeH = PositionedRectangle::proportionalSize; | |||
break; | |||
if (menuResult == 0 || ref == nullptr) | |||
{ | |||
callback (false); | |||
return; | |||
} | |||
case 22: | |||
if (dimension == componentWidth) | |||
sizeW = PositionedRectangle::parentSizeMinusAbsolute; | |||
else | |||
sizeH = PositionedRectangle::parentSizeMinusAbsolute; | |||
break; | |||
switch (menuResult) | |||
{ | |||
case 10: | |||
if (ref->dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentTopLeft; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentTopLeft; | |||
break; | |||
case 11: | |||
if (ref->dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentBottomRight; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentBottomRight; | |||
break; | |||
case 12: | |||
if (ref->dimension == componentX) | |||
xMode = PositionedRectangle::absoluteFromParentCentre; | |||
else | |||
yMode = PositionedRectangle::absoluteFromParentCentre; | |||
break; | |||
case 13: | |||
if (ref->dimension == componentX) | |||
xMode = PositionedRectangle::proportionOfParentSize; | |||
else | |||
yMode = PositionedRectangle::proportionOfParentSize; | |||
break; | |||
case 14: | |||
if (ref->dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtLeftOrTop; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtLeftOrTop; | |||
break; | |||
case 15: | |||
if (ref->dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtCentre; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtCentre; | |||
break; | |||
case 16: | |||
if (ref->dimension == componentX) | |||
xAnchor = PositionedRectangle::anchorAtRightOrBottom; | |||
else | |||
yAnchor = PositionedRectangle::anchorAtRightOrBottom; | |||
break; | |||
case 20: | |||
if (ref->dimension == componentWidth) | |||
sizeW = PositionedRectangle::absoluteSize; | |||
else | |||
sizeH = PositionedRectangle::absoluteSize; | |||
break; | |||
case 21: | |||
if (ref->dimension == componentWidth) | |||
sizeW = PositionedRectangle::proportionalSize; | |||
else | |||
sizeH = PositionedRectangle::proportionalSize; | |||
break; | |||
case 22: | |||
if (ref->dimension == componentWidth) | |||
sizeW = PositionedRectangle::parentSizeMinusAbsolute; | |||
else | |||
sizeH = PositionedRectangle::parentSizeMinusAbsolute; | |||
break; | |||
default: | |||
if (ref->allowRelativeOptions && compLayout != nullptr) | |||
compLayout->processRelativeTargetMenuResult (ref->component, (int) ref->dimension, menuResult); | |||
break; | |||
} | |||
default: | |||
if (allowRelativeOptions && compLayout != nullptr) | |||
compLayout->processRelativeTargetMenuResult (component, (int) dimension, menuResult); | |||
break; | |||
} | |||
const auto parentArea = [&]() -> Rectangle<int> | |||
{ | |||
if (ref->component->findParentComponentOfClass<ComponentLayoutEditor>() != nullptr) | |||
return { ref->component->getParentWidth(), ref->component->getParentHeight() }; | |||
Rectangle<int> parentArea; | |||
if (auto pre = dynamic_cast<PaintRoutineEditor*> (ref->component->getParentComponent())) | |||
return pre->getComponentArea(); | |||
if (component->findParentComponentOfClass<ComponentLayoutEditor>() != nullptr) | |||
parentArea.setSize (component->getParentWidth(), component->getParentHeight()); | |||
else if (auto pre = dynamic_cast<PaintRoutineEditor*> (component->getParentComponent())) | |||
parentArea = pre->getComponentArea(); | |||
else | |||
jassertfalse; | |||
jassertfalse; | |||
return {}; | |||
}(); | |||
int x, xw, y, yh, w, h; | |||
rpr.getRelativeTargetBounds (parentArea, compLayout, x, xw, y, yh, w, h); | |||
int x, xw, y, yh, w, h; | |||
rpr.getRelativeTargetBounds (parentArea, compLayout, x, xw, y, yh, w, h); | |||
PositionedRectangle xyRect (p); | |||
PositionedRectangle whRect (p); | |||
PositionedRectangle xyRect (p); | |||
PositionedRectangle whRect (p); | |||
xyRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, xw, yh)); | |||
xyRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, xw, yh)); | |||
whRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, w, h)); | |||
whRect.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, w, h)); | |||
p.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, xw, yh)); | |||
p.setModes (xAnchor, xMode, yAnchor, yMode, sizeW, sizeH, | |||
Rectangle<int> (x, y, xw, yh)); | |||
p.setX (xyRect.getX()); | |||
p.setY (xyRect.getY()); | |||
p.setWidth (whRect.getWidth()); | |||
p.setHeight (whRect.getHeight()); | |||
p.setX (xyRect.getX()); | |||
p.setY (xyRect.getY()); | |||
p.setWidth (whRect.getWidth()); | |||
p.setHeight (whRect.getHeight()); | |||
if (p != rpr.rect) | |||
{ | |||
rpr.rect = p; | |||
setPosition (rpr); | |||
} | |||
if (p != rpr.rect) | |||
{ | |||
rpr.rect = p; | |||
ref->setPosition (rpr); | |||
} | |||
return true; | |||
callback (true); | |||
}); | |||
} | |||
void resized() | |||
@@ -282,7 +282,7 @@ void ComponentLayoutEditor::mouseDown (const MouseEvent& e) | |||
for (int i = 0; i < ObjectTypes::numComponentTypes; ++i) | |||
m.addCommandItem (commandManager, JucerCommandIDs::newComponentBase + i); | |||
m.show(); | |||
m.showMenuAsync (PopupMenu::Options()); | |||
} | |||
else | |||
{ | |||
@@ -387,7 +387,7 @@ bool ComponentLayoutEditor::isInterestedInDragSource (const SourceDetails& dragS | |||
void ComponentLayoutEditor::itemDropped (const SourceDetails& dragSourceDetails) | |||
{ | |||
OwnedArray <Project::Item> selectedNodes; | |||
OwnedArray<Project::Item> selectedNodes; | |||
ProjectContentComponent::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); | |||
StringArray filenames; | |||
@@ -212,7 +212,7 @@ void PaintRoutineEditor::mouseDown (const MouseEvent& e) | |||
for (int i = 0; i < ObjectTypes::numElementTypes; ++i) | |||
m.addCommandItem (commandManager, JucerCommandIDs::newElementBase + i); | |||
m.show(); | |||
m.showMenuAsync (PopupMenu::Options()); | |||
} | |||
else | |||
{ | |||
@@ -39,7 +39,7 @@ public: | |||
{ | |||
if (auto* r = document.getResources() [row]) | |||
document.getResources().browseForResource ("Select a file to replace this resource", "*", | |||
File (r->originalFilename), r->name); | |||
File (r->originalFilename), r->name, nullptr); | |||
}; | |||
} | |||
@@ -69,7 +69,10 @@ ResourceEditorPanel::ResourceEditorPanel (JucerDocument& doc) | |||
delButton ("Delete selected resources") | |||
{ | |||
addAndMakeVisible (addButton); | |||
addButton.onClick = [this] { document.getResources().browseForResource ("Select a file to add as a resource", "*", {}, {}); }; | |||
addButton.onClick = [this] | |||
{ | |||
document.getResources().browseForResource ("Select a file to add as a resource", "*", {}, {}, nullptr); | |||
}; | |||
addAndMakeVisible (reloadAllButton); | |||
reloadAllButton.onClick = [this] { reloadAll(); }; | |||
@@ -258,16 +261,12 @@ void ResourceEditorPanel::reloadAll() | |||
StringArray failed; | |||
for (int i = 0; i < document.getResources().size(); ++i) | |||
{ | |||
if (! document.getResources().reload (i)) | |||
failed.add (document.getResources().getResourceNames() [i]); | |||
} | |||
if (failed.size() > 0) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, | |||
TRANS("Reloading resources"), | |||
TRANS("The following resources couldn't be reloaded from their original files:\n\n") | |||
+ failed.joinIntoString (", ")); | |||
} | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Reloading resources"), | |||
TRANS("The following resources couldn't be reloaded from their original files:\n\n") | |||
+ failed.joinIntoString (", ")); | |||
} |
@@ -27,14 +27,6 @@ | |||
#include "jucer_JucerDocument.h" | |||
//============================================================================== | |||
BinaryResources::BinaryResources() | |||
{ | |||
} | |||
BinaryResources::~BinaryResources() | |||
{ | |||
} | |||
BinaryResources& BinaryResources::operator= (const BinaryResources& other) | |||
{ | |||
for (auto* r : other.resources) | |||
@@ -130,15 +122,20 @@ bool BinaryResources::reload (const int index) | |||
File (resources [index]->originalFilename)); | |||
} | |||
String BinaryResources::browseForResource (const String& title, | |||
const String& wildcard, | |||
const File& fileToStartFrom, | |||
const String& resourceToReplace) | |||
void BinaryResources::browseForResource (const String& title, | |||
const String& wildcard, | |||
const File& fileToStartFrom, | |||
const String& resourceToReplace, | |||
std::function<void (String)> callback) | |||
{ | |||
FileChooser fc (title, fileToStartFrom, wildcard); | |||
chooser = std::make_unique<FileChooser> (title, fileToStartFrom, wildcard); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; | |||
if (fc.browseForFileToOpen()) | |||
chooser->launchAsync (flags, [this, resourceToReplace, callback] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
callback ({}); | |||
String name (resourceToReplace); | |||
if (name.isEmpty()) | |||
@@ -146,17 +143,15 @@ String BinaryResources::browseForResource (const String& title, | |||
if (! add (name, fc.getResult())) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, | |||
TRANS("Adding Resource"), | |||
TRANS("Failed to load the file!")); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Adding Resource"), | |||
TRANS("Failed to load the file!")); | |||
name.clear(); | |||
} | |||
return name; | |||
} | |||
return {}; | |||
callback (name); | |||
}); | |||
} | |||
String BinaryResources::findUniqueName (const String& rootName) const | |||
@@ -36,9 +36,6 @@ class BinaryResources | |||
{ | |||
public: | |||
//============================================================================== | |||
BinaryResources(); | |||
~BinaryResources(); | |||
BinaryResources& operator= (const BinaryResources& other); | |||
void loadFromCpp (const File& cppFileLocation, const String& cpp); | |||
@@ -57,8 +54,9 @@ public: | |||
void add (const String& name, const String& originalFileName, const MemoryBlock& data); | |||
void remove (const int index); | |||
bool reload (const int index); | |||
String browseForResource (const String& title, const String& wildcard, | |||
const File& fileToStartFrom, const String& resourceToReplace); | |||
void browseForResource (const String& title, const String& wildcard, | |||
const File& fileToStartFrom, const String& resourceToReplace, | |||
std::function<void (String)> callback); | |||
String findUniqueName (const String& rootName) const; | |||
@@ -86,12 +84,13 @@ public: | |||
void fillInGeneratedCode (GeneratedCode& code) const; | |||
private: | |||
//============================================================================== | |||
JucerDocument* document; | |||
OwnedArray <BinaryResource> resources; | |||
BinaryResource* findResource (const String& name) const noexcept; | |||
void changed(); | |||
//============================================================================== | |||
JucerDocument* document; | |||
OwnedArray<BinaryResource> resources; | |||
std::unique_ptr<FileChooser> chooser; | |||
}; |
@@ -122,7 +122,7 @@ public: | |||
private: | |||
JucerDocument* document; | |||
OwnedArray <Component> components; | |||
OwnedArray<Component> components; | |||
SelectedItemSet <Component*> selected; | |||
int nextCompUID; | |||
@@ -692,25 +692,52 @@ public: | |||
{ | |||
} | |||
bool save() override | |||
void saveAsync (std::function<void (bool)> callback) override | |||
{ | |||
return SourceCodeDocument::save() && saveHeader(); | |||
WeakReference<JucerComponentDocument> parent { this }; | |||
SourceCodeDocument::saveAsync ([parent, callback] (bool saveResult) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! saveResult) | |||
{ | |||
callback (false); | |||
return; | |||
} | |||
parent->saveHeaderAsync ([parent, callback] (bool headerSaveResult) | |||
{ | |||
if (parent != nullptr) | |||
callback (headerSaveResult); | |||
}); | |||
}); | |||
} | |||
bool saveHeader() | |||
void saveHeaderAsync (std::function<void (bool)> callback) | |||
{ | |||
auto& odm = ProjucerApplication::getApp().openDocumentManager; | |||
if (auto* header = odm.openFile (nullptr, getFile().withFileExtension (".h"))) | |||
{ | |||
if (header->save()) | |||
WeakReference<JucerComponentDocument> parent { this }; | |||
header->saveAsync ([parent, callback] (bool saveResult) | |||
{ | |||
odm.closeFile (getFile().withFileExtension(".h"), OpenDocumentManager::SaveIfNeeded::no); | |||
return true; | |||
} | |||
if (parent == nullptr) | |||
return; | |||
if (saveResult) | |||
ProjucerApplication::getApp() | |||
.openDocumentManager | |||
.closeFileWithoutSaving (parent->getFile().withFileExtension (".h")); | |||
callback (saveResult); | |||
}); | |||
return; | |||
} | |||
return false; | |||
callback (false); | |||
} | |||
std::unique_ptr<Component> createEditor() override | |||
@@ -733,6 +760,8 @@ public: | |||
bool canOpenFile (const File& f) override { return JucerDocument::isValidJucerCppFile (f); } | |||
Document* openFile (Project* p, const File& f) override { return new JucerComponentDocument (p, f); } | |||
}; | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (JucerComponentDocument) | |||
}; | |||
OpenDocumentManager::DocumentType* createGUIDocumentType(); | |||
@@ -744,53 +773,65 @@ OpenDocumentManager::DocumentType* createGUIDocumentType() | |||
//============================================================================== | |||
struct NewGUIComponentWizard : public NewFileWizard::Type | |||
{ | |||
NewGUIComponentWizard() {} | |||
NewGUIComponentWizard (Project& proj) | |||
: project (proj) | |||
{} | |||
String getName() override { return "GUI Component"; } | |||
void createNewFile (Project& project, Project::Item parent) override | |||
void createNewFile (Project& p, Project::Item parent) override | |||
{ | |||
auto newFile = askUserToChooseNewFile (String (defaultClassName) + ".h", "*.h;*.cpp", parent); | |||
jassert (&p == &project); | |||
if (newFile != File()) | |||
askUserToChooseNewFile (String (defaultClassName) + ".h", "*.h;*.cpp", parent, [this, parent] (File newFile) mutable | |||
{ | |||
auto headerFile = newFile.withFileExtension (".h"); | |||
auto cppFile = newFile.withFileExtension (".cpp"); | |||
if (newFile != File()) | |||
{ | |||
auto headerFile = newFile.withFileExtension (".h"); | |||
auto cppFile = newFile.withFileExtension (".cpp"); | |||
headerFile.replaceWithText (String()); | |||
cppFile.replaceWithText (String()); | |||
headerFile.replaceWithText (String()); | |||
cppFile.replaceWithText (String()); | |||
auto& odm = ProjucerApplication::getApp().openDocumentManager; | |||
auto& odm = ProjucerApplication::getApp().openDocumentManager; | |||
if (auto* cpp = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, cppFile))) | |||
{ | |||
if (auto* header = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, headerFile))) | |||
if (auto* cpp = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, cppFile))) | |||
{ | |||
std::unique_ptr<JucerDocument> jucerDoc (new ComponentDocument (cpp)); | |||
if (jucerDoc != nullptr) | |||
if (auto* header = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, headerFile))) | |||
{ | |||
jucerDoc->setClassName (newFile.getFileNameWithoutExtension()); | |||
jucerDoc->flushChangesToDocuments (&project, true); | |||
jucerDoc.reset(); | |||
cpp->save(); | |||
header->save(); | |||
odm.closeDocument (cpp, OpenDocumentManager::SaveIfNeeded::yes); | |||
odm.closeDocument (header, OpenDocumentManager::SaveIfNeeded::yes); | |||
parent.addFileRetainingSortOrder (headerFile, true); | |||
parent.addFileRetainingSortOrder (cppFile, true); | |||
std::unique_ptr<JucerDocument> jucerDoc (new ComponentDocument (cpp)); | |||
if (jucerDoc != nullptr) | |||
{ | |||
jucerDoc->setClassName (newFile.getFileNameWithoutExtension()); | |||
jucerDoc->flushChangesToDocuments (&project, true); | |||
jucerDoc.reset(); | |||
for (auto* doc : { cpp, header }) | |||
{ | |||
doc->saveAsync ([doc] (bool) | |||
{ | |||
ProjucerApplication::getApp() | |||
.openDocumentManager | |||
.closeDocumentAsync (doc, OpenDocumentManager::SaveIfNeeded::yes, nullptr); | |||
}); | |||
} | |||
parent.addFileRetainingSortOrder (headerFile, true); | |||
parent.addFileRetainingSortOrder (cppFile, true); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
Project& project; | |||
}; | |||
NewFileWizard::Type* createGUIComponentWizard(); | |||
NewFileWizard::Type* createGUIComponentWizard() | |||
NewFileWizard::Type* createGUIComponentWizard (Project&); | |||
NewFileWizard::Type* createGUIComponentWizard (Project& p) | |||
{ | |||
return new NewGUIComponentWizard(); | |||
return new NewGUIComponentWizard (p); | |||
} |
@@ -109,7 +109,7 @@ public: | |||
//============================================================================== | |||
private: | |||
OwnedArray <PaintElement> elements; | |||
OwnedArray<PaintElement> elements; | |||
SelectedItemSet <PaintElement*> selectedElements; | |||
SelectedItemSet <PathPoint*> selectedPoints; | |||
JucerDocument* document; | |||
@@ -664,15 +664,16 @@ void EnabledModulesList::addModuleInteractive (const String& moduleID) | |||
void EnabledModulesList::addModuleFromUserSelectedFile() | |||
{ | |||
auto lastLocation = getDefaultModulesFolder(); | |||
chooser = std::make_unique<FileChooser> ("Select a module to add...", getDefaultModulesFolder(), ""); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
FileChooser fc ("Select a module to add...", lastLocation, {}); | |||
if (fc.browseForDirectory()) | |||
chooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
lastLocation = fc.getResult(); | |||
addModuleOfferingToCopy (lastLocation, true); | |||
} | |||
if (fc.getResult() == File{}) | |||
return; | |||
addModuleOfferingToCopy (fc.getResult(), true); | |||
}); | |||
} | |||
void EnabledModulesList::addModuleOfferingToCopy (const File& f, bool isFromUserSpecifiedFolder) | |||
@@ -142,5 +142,7 @@ private: | |||
CriticalSection stateLock; | |||
ValueTree state; | |||
std::unique_ptr<FileChooser> chooser; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EnabledModulesList) | |||
}; |
@@ -78,13 +78,25 @@ public: | |||
void deleteItem() override | |||
{ | |||
if (AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Delete Exporter", | |||
"Are you sure you want to delete this export target?")) | |||
WeakReference<ExporterItem> safeThis { this }; | |||
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
"Delete Exporter", | |||
"Are you sure you want to delete this export target?", | |||
"", | |||
"", | |||
nullptr, | |||
ModalCallbackFunction::create ([safeThis] (int result) | |||
{ | |||
closeSettingsPage(); | |||
ValueTree parent (exporter->settings.getParent()); | |||
parent.removeChild (exporter->settings, project.getUndoManagerFor (parent)); | |||
} | |||
if (safeThis == nullptr) | |||
return; | |||
if (result == 0) | |||
return; | |||
safeThis->closeSettingsPage(); | |||
auto parent = safeThis->exporter->settings.getParent(); | |||
parent.removeChild (safeThis->exporter->settings, safeThis->project.getUndoManagerFor (parent)); | |||
})); | |||
} | |||
void addSubItems() override | |||
@@ -117,7 +129,7 @@ public: | |||
if (resultCode == 1) | |||
exporter->addNewConfiguration (false); | |||
else if (resultCode == 2) | |||
project.saveProject (exporter.get()); | |||
project.saveProject (Async::yes, exporter.get(), nullptr); | |||
else if (resultCode == 3) | |||
deleteAllSelectedItems(); | |||
} | |||
@@ -200,6 +212,7 @@ private: | |||
}; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ExporterItem) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (ExporterItem) | |||
}; | |||
@@ -231,12 +244,24 @@ public: | |||
void deleteItem() override | |||
{ | |||
if (AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Delete Configuration", | |||
"Are you sure you want to delete this configuration?")) | |||
WeakReference<ConfigItem> parent { this }; | |||
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
"Delete Configuration", | |||
"Are you sure you want to delete this configuration?", | |||
"", | |||
"", | |||
nullptr, | |||
ModalCallbackFunction::create ([parent] (int result) | |||
{ | |||
closeSettingsPage(); | |||
config->removeFromExporter(); | |||
} | |||
if (parent == nullptr) | |||
return; | |||
if (result == 0) | |||
return; | |||
parent->closeSettingsPage(); | |||
parent->config->removeFromExporter(); | |||
})); | |||
} | |||
void showPopupMenu (Point<int> p) override | |||
@@ -297,6 +322,8 @@ private: | |||
}; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConfigItem) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (ConfigItem) | |||
}; | |||
//============================================================================== | |||
@@ -84,40 +84,26 @@ public: | |||
} | |||
} | |||
if (filesToTrash.size() > 0) | |||
{ | |||
String fileList; | |||
auto maxFilesToList = 10; | |||
for (auto i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;) | |||
fileList << filesToTrash.getUnchecked(i).getFullPathName() << "\n"; | |||
if (filesToTrash.size() > maxFilesToList) | |||
fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files..."; | |||
auto r = AlertWindow::showYesNoCancelBox (AlertWindow::NoIcon, "Delete Project Items", | |||
"As well as removing the selected item(s) from the project, do you also want to move their files to the trash:\n\n" | |||
+ fileList, | |||
"Just remove references", | |||
"Also move files to Trash", | |||
"Cancel", | |||
tree->getTopLevelComponent()); | |||
WeakReference<FileTreeItemBase> treeRootItem { dynamic_cast<FileTreeItemBase*> (tree->getRootItem()) }; | |||
if (r == 0) | |||
return; | |||
if (r != 2) | |||
filesToTrash.clear(); | |||
if (treeRootItem == nullptr) | |||
{ | |||
jassertfalse; | |||
return; | |||
} | |||
if (auto* treeRootItem = dynamic_cast<FileTreeItemBase*> (tree->getRootItem())) | |||
auto doDelete = [treeRootItem, itemsToRemove] (const Array<File>& fsToTrash) | |||
{ | |||
if (treeRootItem == nullptr) | |||
return; | |||
auto& om = ProjucerApplication::getApp().openDocumentManager; | |||
for (auto i = filesToTrash.size(); --i >= 0;) | |||
for (auto i = fsToTrash.size(); --i >= 0;) | |||
{ | |||
auto f = filesToTrash.getUnchecked(i); | |||
auto f = fsToTrash.getUnchecked(i); | |||
om.closeFile (f, OpenDocumentManager::SaveIfNeeded::no); | |||
om.closeFileWithoutSaving (f); | |||
if (! f.moveToTrash()) | |||
{ | |||
@@ -136,15 +122,48 @@ public: | |||
pcc->hideEditor(); | |||
} | |||
om.closeFile (itemToRemove->getFile(), OpenDocumentManager::SaveIfNeeded::no); | |||
om.closeFileWithoutSaving (itemToRemove->getFile()); | |||
itemToRemove->deleteItem(); | |||
} | |||
} | |||
} | |||
else | |||
}; | |||
if (! filesToTrash.isEmpty()) | |||
{ | |||
jassertfalse; | |||
String fileList; | |||
auto maxFilesToList = 10; | |||
for (auto i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;) | |||
fileList << filesToTrash.getUnchecked(i).getFullPathName() << "\n"; | |||
if (filesToTrash.size() > maxFilesToList) | |||
fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files..."; | |||
AlertWindow::showYesNoCancelBox (AlertWindow::NoIcon, | |||
"Delete Project Items", | |||
"As well as removing the selected item(s) from the project, do you also want to move their files to the trash:\n\n" | |||
+ fileList, | |||
"Just remove references", | |||
"Also move files to Trash", | |||
"Cancel", | |||
tree->getTopLevelComponent(), | |||
ModalCallbackFunction::create ([treeRootItem, filesToTrash, doDelete] (int r) mutable | |||
{ | |||
if (treeRootItem == nullptr) | |||
return; | |||
if (r == 0) | |||
return; | |||
if (r != 2) | |||
filesToTrash.clear(); | |||
doDelete (filesToTrash); | |||
})); | |||
return; | |||
} | |||
doDelete (filesToTrash); | |||
} | |||
virtual void revealInFinder() const | |||
@@ -155,17 +174,24 @@ public: | |||
virtual void browseToAddExistingFiles() | |||
{ | |||
auto location = item.isGroup() ? item.determineGroupFolder() : getFile(); | |||
FileChooser fc ("Add Files to Jucer Project", location, {}); | |||
chooser = std::make_unique<FileChooser> ("Add Files to Jucer Project", location, ""); | |||
auto flags = FileBrowserComponent::openMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::canSelectDirectories | |||
| FileBrowserComponent::canSelectMultipleItems; | |||
if (fc.browseForMultipleFilesOrDirectories()) | |||
chooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResults().isEmpty()) | |||
return; | |||
StringArray files; | |||
for (int i = 0; i < fc.getResults().size(); ++i) | |||
files.add (fc.getResults().getReference(i).getFullPathName()); | |||
addFilesRetainingSortOrder (files); | |||
} | |||
}); | |||
} | |||
virtual void checkFileStatus() // (recursive) | |||
@@ -192,7 +218,7 @@ public: | |||
p->addFilesRetainingSortOrder (files); | |||
} | |||
virtual void moveSelectedItemsTo (OwnedArray <Project::Item>&, int /*insertIndex*/) | |||
virtual void moveSelectedItemsTo (OwnedArray<Project::Item>&, int /*insertIndex*/) | |||
{ | |||
jassertfalse; | |||
} | |||
@@ -269,7 +295,7 @@ public: | |||
void filesDropped (const StringArray& files, int insertIndex) override | |||
{ | |||
if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension)) | |||
ProjucerApplication::getApp().openFile (files[0]); | |||
ProjucerApplication::getApp().openFile (files[0], [] (bool) {}); | |||
else | |||
addFilesAtIndex (files, insertIndex); | |||
} | |||
@@ -444,6 +470,11 @@ protected: | |||
return -1; | |||
} | |||
private: | |||
std::unique_ptr<FileChooser> chooser; | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (FileTreeItemBase) | |||
}; | |||
//============================================================================== | |||
@@ -456,7 +487,7 @@ public: | |||
} | |||
bool acceptsFileDrop (const StringArray&) const override { return false; } | |||
bool acceptsDragItems (const OwnedArray <Project::Item>&) override { return false; } | |||
bool acceptsDragItems (const OwnedArray<Project::Item>&) override { return false; } | |||
String getDisplayName() const override | |||
{ | |||
@@ -490,8 +521,9 @@ public: | |||
{ | |||
if (newName != File::createLegalFileName (newName)) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", | |||
"That filename contained some illegal characters!"); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"File Rename", | |||
"That filename contained some illegal characters!"); | |||
triggerAsyncRename (item); | |||
return; | |||
} | |||
@@ -506,30 +538,42 @@ public: | |||
if (correspondingItem.isValid()) | |||
{ | |||
if (AlertWindow::showOkCancelBox (AlertWindow::NoIcon, "File Rename", | |||
"Do you also want to rename the corresponding file \"" + correspondingFile.getFileName() | |||
+ "\" to match?")) | |||
WeakReference<SourceFileItem> parent { this }; | |||
AlertWindow::showOkCancelBox (AlertWindow::NoIcon, | |||
"File Rename", | |||
"Do you also want to rename the corresponding file \"" + correspondingFile.getFileName() + "\" to match?", | |||
{}, | |||
{}, | |||
nullptr, | |||
ModalCallbackFunction::create ([parent, oldFile, newFile, correspondingFile, correspondingItem] (int result) mutable | |||
{ | |||
if (! item.renameFile (newFile)) | |||
if (parent == nullptr || result == 0) | |||
return; | |||
if (! parent->item.renameFile (newFile)) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", | |||
"Failed to rename \"" + oldFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"File Rename", | |||
"Failed to rename \"" + oldFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); | |||
return; | |||
} | |||
if (! correspondingItem.renameFile (newFile.withFileExtension (correspondingFile.getFileExtension()))) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", | |||
"Failed to rename \"" + correspondingFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"File Rename", | |||
"Failed to rename \"" + correspondingFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); | |||
} | |||
} | |||
})); | |||
} | |||
} | |||
if (! item.renameFile (newFile)) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", | |||
"Failed to rename the file!\n\nCheck your file permissions!"); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"File Rename", | |||
"Failed to rename the file!\n\nCheck your file permissions!"); | |||
} | |||
} | |||
@@ -600,6 +644,8 @@ public: | |||
break; | |||
} | |||
} | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (SourceFileItem) | |||
}; | |||
//============================================================================== | |||
@@ -799,7 +845,7 @@ public: | |||
m.addItem (1002, "Add Existing Files..."); | |||
m.addSeparator(); | |||
NewFileWizard().addWizardsToMenu (m); | |||
wizard.addWizardsToMenu (m); | |||
} | |||
void processCreateFileMenuItem (int menuID) | |||
@@ -811,7 +857,7 @@ public: | |||
default: | |||
jassert (getProject() != nullptr); | |||
NewFileWizard().runWizardFromMenu (menuID, *getProject(), item); | |||
wizard.runWizardFromMenu (menuID, *getProject(), item); | |||
break; | |||
} | |||
} | |||
@@ -832,4 +878,5 @@ public: | |||
} | |||
String searchFilter; | |||
NewFileWizard wizard; | |||
}; |
@@ -270,7 +270,7 @@ private: | |||
Array<Value> exporterModulePathValues, globalPathValues; | |||
Value useGlobalPathValue; | |||
OwnedArray <Project::ConfigFlag> configFlags; | |||
OwnedArray<Project::ConfigFlag> configFlags; | |||
PropertyGroupComponent group; | |||
Project& project; | |||
@@ -248,7 +248,7 @@ void HeaderComponent::initialiseButtons() | |||
else | |||
{ | |||
if (auto exporter = getSelectedExporter()) | |||
project->openProjectInIDE (*exporter, true); | |||
project->openProjectInIDE (*exporter, true, nullptr); | |||
} | |||
} | |||
}; | |||
@@ -28,7 +28,12 @@ | |||
#include "Sidebar/jucer_Sidebar.h" | |||
NewFileWizard::Type* createGUIComponentWizard(); | |||
struct WizardHolder | |||
{ | |||
std::unique_ptr<NewFileWizard::Type> wizard; | |||
}; | |||
NewFileWizard::Type* createGUIComponentWizard (Project&); | |||
//============================================================================== | |||
ProjectContentComponent::ProjectContentComponent() | |||
@@ -305,7 +310,7 @@ void ProjectContentComponent::closeDocument() | |||
if (currentDocument != nullptr) | |||
{ | |||
ProjucerApplication::getApp().openDocumentManager | |||
.closeDocument (currentDocument, OpenDocumentManager::SaveIfNeeded::yes); | |||
.closeDocumentAsync (currentDocument, OpenDocumentManager::SaveIfNeeded::yes, nullptr); | |||
return; | |||
} | |||
@@ -315,35 +320,49 @@ void ProjectContentComponent::closeDocument() | |||
static void showSaveWarning (OpenDocumentManager::Document* currentDocument) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, | |||
TRANS("Save failed!"), | |||
TRANS("Couldn't save the file:") | |||
+ "\n" + currentDocument->getFile().getFullPathName()); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Save failed!"), | |||
TRANS("Couldn't save the file:") | |||
+ "\n" + currentDocument->getFile().getFullPathName()); | |||
} | |||
void ProjectContentComponent::saveDocument() | |||
void ProjectContentComponent::saveDocumentAsync() | |||
{ | |||
if (currentDocument != nullptr) | |||
{ | |||
if (! currentDocument->save()) | |||
showSaveWarning (currentDocument); | |||
SafePointer<ProjectContentComponent> parent { this }; | |||
currentDocument->saveAsync ([parent] (bool savedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! savedSuccessfully) | |||
showSaveWarning (parent->currentDocument); | |||
refreshProjectTreeFileStatuses(); | |||
parent->refreshProjectTreeFileStatuses(); | |||
}); | |||
} | |||
else | |||
{ | |||
saveProject(); | |||
saveProjectAsync(); | |||
} | |||
} | |||
void ProjectContentComponent::saveAs() | |||
void ProjectContentComponent::saveAsAsync() | |||
{ | |||
if (currentDocument != nullptr) | |||
{ | |||
if (! currentDocument->saveAs()) | |||
showSaveWarning (currentDocument); | |||
SafePointer<ProjectContentComponent> parent { this }; | |||
currentDocument->saveAsAsync ([parent] (bool savedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! savedSuccessfully) | |||
showSaveWarning (parent->currentDocument); | |||
refreshProjectTreeFileStatuses(); | |||
parent->refreshProjectTreeFileStatuses(); | |||
}); | |||
} | |||
} | |||
@@ -381,18 +400,16 @@ bool ProjectContentComponent::goToCounterpart() | |||
return false; | |||
} | |||
bool ProjectContentComponent::saveProject() | |||
void ProjectContentComponent::saveProjectAsync() | |||
{ | |||
if (project != nullptr) | |||
return (project->save (true, true) == FileBasedDocument::savedOk); | |||
return false; | |||
project->saveAsync (true, true, nullptr); | |||
} | |||
void ProjectContentComponent::closeProject() | |||
{ | |||
if (auto* mw = findParentComponentOfClass<MainWindow>()) | |||
mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes); | |||
mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, nullptr); | |||
} | |||
void ProjectContentComponent::showProjectSettings() | |||
@@ -481,7 +498,7 @@ void ProjectContentComponent::openInSelectedIDE (bool saveFirst) | |||
{ | |||
if (project != nullptr) | |||
if (auto selectedExporter = headerComponent.getSelectedExporter()) | |||
project->openProjectInIDE (*selectedExporter, saveFirst); | |||
project->openProjectInIDE (*selectedExporter, saveFirst, nullptr); | |||
} | |||
void ProjectContentComponent::showNewExporterMenu() | |||
@@ -787,7 +804,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica | |||
bool ProjectContentComponent::perform (const InvocationInfo& info) | |||
{ | |||
// don't allow the project to be saved again if it's currently saving | |||
if (isSaveCommand (info.commandID) && (project != nullptr && project->isCurrentlySaving())) | |||
if (isSaveCommand (info.commandID) && project != nullptr && project->isCurrentlySaving()) | |||
return false; | |||
switch (info.commandID) | |||
@@ -818,14 +835,14 @@ bool ProjectContentComponent::perform (const InvocationInfo& info) | |||
switch (info.commandID) | |||
{ | |||
case CommandIDs::saveProject: saveProject(); break; | |||
case CommandIDs::closeProject: closeProject(); break; | |||
case CommandIDs::saveDocument: saveDocument(); break; | |||
case CommandIDs::saveDocumentAs: saveAs(); break; | |||
case CommandIDs::closeDocument: closeDocument(); break; | |||
case CommandIDs::goToPreviousDoc: goToPreviousFile(); break; | |||
case CommandIDs::goToNextDoc: goToNextFile(); break; | |||
case CommandIDs::goToCounterpart: goToCounterpart(); break; | |||
case CommandIDs::saveProject: saveProjectAsync(); break; | |||
case CommandIDs::closeProject: closeProject(); break; | |||
case CommandIDs::saveDocument: saveDocumentAsync(); break; | |||
case CommandIDs::saveDocumentAs: saveAsAsync(); break; | |||
case CommandIDs::closeDocument: closeDocument(); break; | |||
case CommandIDs::goToPreviousDoc: goToPreviousFile(); break; | |||
case CommandIDs::goToNextDoc: goToNextFile(); break; | |||
case CommandIDs::goToCounterpart: goToCounterpart(); break; | |||
case CommandIDs::showProjectSettings: showProjectSettings(); break; | |||
case CommandIDs::showFileExplorerPanel: showFilesPanel(); break; | |||
@@ -866,8 +883,9 @@ void ProjectContentComponent::addNewGUIFile() | |||
{ | |||
if (project != nullptr) | |||
{ | |||
std::unique_ptr<NewFileWizard::Type> wizard (createGUIComponentWizard()); | |||
wizard->createNewFile (*project, project->getMainGroup()); | |||
wizardHolder = std::make_unique<WizardHolder>(); | |||
wizardHolder->wizard.reset (createGUIComponentWizard (*project)); | |||
wizardHolder->wizard->createNewFile (*project, project->getMainGroup()); | |||
} | |||
} | |||
@@ -31,6 +31,7 @@ | |||
#include "jucer_ContentViewComponent.h" | |||
class Sidebar; | |||
struct WizardHolder; | |||
//============================================================================== | |||
class ProjectContentComponent : public Component, | |||
@@ -57,8 +58,8 @@ public: | |||
void hideDocument (OpenDocumentManager::Document*); | |||
OpenDocumentManager::Document* getCurrentDocument() const { return currentDocument; } | |||
void closeDocument(); | |||
void saveDocument(); | |||
void saveAs(); | |||
void saveDocumentAsync(); | |||
void saveAsAsync(); | |||
void hideEditor(); | |||
void setScrollableEditorComponent (std::unique_ptr<Component> component); | |||
@@ -72,7 +73,7 @@ public: | |||
bool canGoToCounterpart() const; | |||
bool goToCounterpart(); | |||
bool saveProject(); | |||
void saveProjectAsync(); | |||
void closeProject(); | |||
void openInSelectedIDE (bool saveFirst); | |||
void showNewExporterMenu(); | |||
@@ -145,6 +146,8 @@ private: | |||
bool isForeground = false; | |||
int lastViewedTab = 0; | |||
std::unique_ptr<WizardHolder> wizardHolder; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectContentComponent) | |||
}; |
@@ -66,12 +66,22 @@ void Project::ProjectFileModificationPoller::reloadProjectFromDisk() | |||
{ | |||
if (auto* mw = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (projectFile)) | |||
{ | |||
mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::no); | |||
mw->openFile (projectFile); | |||
Component::SafePointer<MainWindow> parent { mw }; | |||
mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::no, [parent, oldTemporaryDirectory, projectFile] (bool) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (oldTemporaryDirectory != File()) | |||
if (auto* newProject = mw->getProject()) | |||
newProject->setTemporaryDirectory (oldTemporaryDirectory); | |||
parent->openFile (projectFile, [parent, oldTemporaryDirectory] (bool openedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (openedSuccessfully && oldTemporaryDirectory != File()) | |||
if (auto* newProject = parent->getProject()) | |||
newProject->setTemporaryDirectory (oldTemporaryDirectory); | |||
}); | |||
}); | |||
} | |||
}); | |||
} | |||
@@ -79,7 +89,7 @@ void Project::ProjectFileModificationPoller::reloadProjectFromDisk() | |||
void Project::ProjectFileModificationPoller::resaveProject() | |||
{ | |||
reset(); | |||
project.saveProject(); | |||
project.saveProject (Async::yes, nullptr, nullptr); | |||
} | |||
//============================================================================== | |||
@@ -122,7 +132,7 @@ Project::~Project() | |||
auto& app = ProjucerApplication::getApp(); | |||
app.openDocumentManager.closeAllDocumentsUsingProject (*this, OpenDocumentManager::SaveIfNeeded::no); | |||
app.openDocumentManager.closeAllDocumentsUsingProjectWithoutSaving (*this); | |||
if (! app.isRunningCommandLine) | |||
app.getLicenseController().removeListener (this); | |||
@@ -397,9 +407,9 @@ void Project::removeDefunctExporters() | |||
if (ProjucerApplication::getApp().isRunningCommandLine) | |||
std::cout << "WARNING! The " + oldExporters[key] + " Exporter is deprecated. The exporter will be removed from this project." << std::endl; | |||
else | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, | |||
TRANS (oldExporters[key]), | |||
TRANS ("The " + oldExporters[key] + " Exporter is deprecated. The exporter will be removed from this project.")); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS (oldExporters[key]), | |||
TRANS ("The " + oldExporters[key] + " Exporter is deprecated. The exporter will be removed from this project.")); | |||
exporters.removeChild (oldExporter, nullptr); | |||
} | |||
@@ -664,40 +674,73 @@ Result Project::saveDocument (const File& file) | |||
jassert (file == getFile()); | |||
ignoreUnused (file); | |||
return saveProject(); | |||
auto sharedResult = Result::ok(); | |||
saveProject (Async::no, nullptr, [&sharedResult] (Result actualResult) | |||
{ | |||
sharedResult = actualResult; | |||
}); | |||
return sharedResult; | |||
} | |||
void Project::saveDocumentAsync (const File& file, std::function<void (Result)> afterSave) | |||
{ | |||
jassert (file == getFile()); | |||
ignoreUnused (file); | |||
saveProject (Async::yes, nullptr, std::move (afterSave)); | |||
} | |||
Result Project::saveProject (ProjectExporter* exporterToSave) | |||
void Project::saveProject (Async async, | |||
ProjectExporter* exporterToSave, | |||
std::function<void (Result)> onCompletion) | |||
{ | |||
if (isSaveAndExportDisabled()) | |||
return Result::fail ("Save and export is disabled."); | |||
{ | |||
onCompletion (Result::fail ("Save and export is disabled.")); | |||
return; | |||
} | |||
if (isSaving) | |||
return Result::ok(); | |||
if (saver != nullptr) | |||
{ | |||
onCompletion (Result::ok()); | |||
return; | |||
} | |||
if (isTemporaryProject()) | |||
{ | |||
saveAndMoveTemporaryProject (false); | |||
return Result::ok(); | |||
onCompletion (Result::ok()); | |||
return; | |||
} | |||
updateProjectSettings(); | |||
if (! ProjucerApplication::getApp().isRunningCommandLine) | |||
{ | |||
ProjucerApplication::getApp().openDocumentManager.saveAll(); | |||
ProjucerApplication::getApp().openDocumentManager.saveAllSyncWithoutAsking(); | |||
if (! isTemporaryProject()) | |||
registerRecentFile (getFile()); | |||
} | |||
const ScopedValueSetter<bool> vs (isSaving, true, false); | |||
WeakReference<Project> ref (this); | |||
saver = std::make_unique<ProjectSaver> (*this); | |||
saver->save (async, exporterToSave, [ref, onCompletion] (Result result) | |||
{ | |||
if (ref == nullptr) | |||
return; | |||
ProjectSaver saver (*this); | |||
return saver.save (exporterToSave); | |||
ref->saver = nullptr; | |||
if (onCompletion != nullptr) | |||
onCompletion (result); | |||
}); | |||
} | |||
Result Project::openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst) | |||
void Project::openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst, std::function<void (Result)> onCompletion) | |||
{ | |||
for (ExporterIterator exporter (*this); exporter.next();) | |||
{ | |||
@@ -706,33 +749,57 @@ Result Project::openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirs | |||
if (isTemporaryProject()) | |||
{ | |||
saveAndMoveTemporaryProject (true); | |||
return Result::ok(); | |||
if (onCompletion != nullptr) | |||
onCompletion (Result::ok()); | |||
return; | |||
} | |||
if (saveFirst) | |||
{ | |||
auto result = saveProject(); | |||
struct Callback | |||
{ | |||
void operator() (Result saveResult) noexcept | |||
{ | |||
if (! saveResult.wasOk()) | |||
{ | |||
if (onCompletion != nullptr) | |||
onCompletion (saveResult); | |||
return; | |||
} | |||
// Workaround for a bug where Xcode thinks the project is invalid if opened immediately | |||
// after writing | |||
auto exporterCopy = exporter; | |||
Timer::callAfterDelay (exporter->isXcode() ? 1000 : 0, [exporterCopy] | |||
{ | |||
exporterCopy->launchProject(); | |||
}); | |||
} | |||
if (! result.wasOk()) | |||
return result; | |||
} | |||
std::shared_ptr<ProjectExporter> exporter; | |||
std::function<void (Result)> onCompletion; | |||
}; | |||
// Workaround for a bug where Xcode thinks the project is invalid if opened immediately | |||
// after writing | |||
if (saveFirst && exporter->isXcode()) | |||
Thread::sleep (1000); | |||
saveProject (Async::yes, nullptr, Callback { std::move (exporter.exporter), onCompletion }); | |||
return; | |||
} | |||
exporter->launchProject(); | |||
break; | |||
} | |||
} | |||
return Result::ok(); | |||
if (onCompletion != nullptr) | |||
onCompletion (Result::ok()); | |||
} | |||
Result Project::saveResourcesOnly() | |||
{ | |||
ProjectSaver saver (*this); | |||
return saver.saveResourcesOnly(); | |||
saver = std::make_unique<ProjectSaver> (*this); | |||
return saver->saveResourcesOnly(); | |||
} | |||
bool Project::hasIncompatibleLicenseTypeAndSplashScreenSetting() const | |||
@@ -988,37 +1055,41 @@ void Project::setTemporaryDirectory (const File& dir) noexcept | |||
void Project::saveAndMoveTemporaryProject (bool openInIDE) | |||
{ | |||
FileChooser fc ("Save Project"); | |||
fc.browseForDirectory(); | |||
chooser = std::make_unique<FileChooser> ("Save Project"); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
auto newParentDirectory = fc.getResult(); | |||
if (! newParentDirectory.exists()) | |||
return; | |||
auto newDirectory = newParentDirectory.getChildFile (tempDirectory.getFileName()); | |||
auto oldJucerFileName = getFile().getFileName(); | |||
ProjectSaver saver (*this); | |||
saver.save(); | |||
chooser->launchAsync (flags, [this, openInIDE] (const FileChooser& fc) | |||
{ | |||
auto newParentDirectory = fc.getResult(); | |||
tempDirectory.copyDirectoryTo (newDirectory); | |||
tempDirectory.deleteRecursively(); | |||
tempDirectory = File(); | |||
if (! newParentDirectory.exists()) | |||
return; | |||
// reload project from new location | |||
if (auto* window = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())) | |||
{ | |||
Component::SafePointer<MainWindow> safeWindow (window); | |||
auto newDirectory = newParentDirectory.getChildFile (tempDirectory.getFileName()); | |||
auto oldJucerFileName = getFile().getFileName(); | |||
MessageManager::callAsync ([safeWindow, newDirectory, oldJucerFileName, openInIDE]() mutable | |||
saver = std::make_unique<ProjectSaver> (*this); | |||
saver->save (Async::yes, nullptr, [this, newDirectory, oldJucerFileName, openInIDE] (Result) | |||
{ | |||
if (safeWindow != nullptr) | |||
safeWindow->moveProject (newDirectory.getChildFile (oldJucerFileName), | |||
openInIDE ? MainWindow::OpenInIDE::yes | |||
: MainWindow::OpenInIDE::no); | |||
tempDirectory.copyDirectoryTo (newDirectory); | |||
tempDirectory.deleteRecursively(); | |||
tempDirectory = File(); | |||
// reload project from new location | |||
if (auto* window = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())) | |||
{ | |||
Component::SafePointer<MainWindow> safeWindow (window); | |||
MessageManager::callAsync ([safeWindow, newDirectory, oldJucerFileName, openInIDE]() mutable | |||
{ | |||
if (safeWindow != nullptr) | |||
safeWindow->moveProject (newDirectory.getChildFile (oldJucerFileName), | |||
openInIDE ? MainWindow::OpenInIDE::yes | |||
: MainWindow::OpenInIDE::no); | |||
}); | |||
} | |||
}); | |||
} | |||
}); | |||
} | |||
//============================================================================== | |||
@@ -2620,7 +2691,6 @@ StringPairArray Project::getAudioPluginFlags() const | |||
//============================================================================== | |||
Project::ExporterIterator::ExporterIterator (Project& p) : index (-1), project (p) {} | |||
Project::ExporterIterator::~ExporterIterator() {} | |||
bool Project::ExporterIterator::next() | |||
{ | |||
@@ -31,6 +31,7 @@ | |||
class ProjectExporter; | |||
class LibraryModule; | |||
class EnabledModulesList; | |||
class ProjectSaver; | |||
namespace ProjectMessages | |||
{ | |||
@@ -109,6 +110,8 @@ namespace ProjectMessages | |||
using MessageAction = std::pair<String, std::function<void()>>; | |||
} | |||
enum class Async { no, yes }; | |||
//============================================================================== | |||
class Project : public FileBasedDocument, | |||
private ValueTree::Listener, | |||
@@ -125,10 +128,11 @@ public: | |||
String getDocumentTitle() override; | |||
Result loadDocument (const File& file) override; | |||
Result saveDocument (const File& file) override; | |||
void saveDocumentAsync (const File& file, std::function<void (Result)> callback) override; | |||
Result saveProject (ProjectExporter* exporterToSave = nullptr); | |||
void saveProject (Async, ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion); | |||
Result saveResourcesOnly(); | |||
Result openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst); | |||
void openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst, std::function<void (Result)> onCompletion); | |||
File getLastDocumentOpened() override; | |||
void setLastDocumentOpened (const File& file) override; | |||
@@ -433,7 +437,6 @@ public: | |||
struct ExporterIterator | |||
{ | |||
ExporterIterator (Project& project); | |||
~ExporterIterator(); | |||
bool next(); | |||
@@ -486,7 +489,7 @@ public: | |||
String getUniqueTargetFolderSuffixForExporter (const Identifier& exporterIdentifier, const String& baseTargetFolder); | |||
//============================================================================== | |||
bool isCurrentlySaving() const noexcept { return isSaving; } | |||
bool isCurrentlySaving() const noexcept { return saver != nullptr; } | |||
bool isTemporaryProject() const noexcept { return tempDirectory != File(); } | |||
File getTemporaryDirectory() const noexcept { return tempDirectory; } | |||
@@ -572,7 +575,6 @@ private: | |||
//============================================================================== | |||
friend class Item; | |||
bool isSaving = false; | |||
StringPairArray parsedPreprocessorDefs; | |||
//============================================================================== | |||
@@ -612,12 +614,21 @@ private: | |||
void updateCLionWarning (bool showWarning); | |||
void updateModuleNotFoundWarning (bool showWarning); | |||
void openProjectInIDEImpl (ExporterIterator exporter, | |||
String exporterToOpen, | |||
bool saveFirst, | |||
std::function<void (Result)> onCompletion); | |||
ValueTree projectMessages { ProjectMessages::Ids::projectMessages, {}, | |||
{ { ProjectMessages::Ids::notification, {} }, { ProjectMessages::Ids::warning, {} } } }; | |||
std::map<Identifier, std::vector<ProjectMessages::MessageAction>> messageActions; | |||
ProjectFileModificationPoller fileModificationPoller { *this }; | |||
std::unique_ptr<FileChooser> chooser; | |||
std::unique_ptr<ProjectSaver> saver; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Project) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (Project) | |||
}; |
@@ -42,17 +42,32 @@ ProjectSaver::ProjectSaver (Project& p) | |||
generatedFilesGroup.setID (generatedGroupID); | |||
} | |||
Result ProjectSaver::save (ProjectExporter* exporterToSave) | |||
void ProjectSaver::save (Async async, ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion) | |||
{ | |||
if (! ProjucerApplication::getApp().isRunningCommandLine) | |||
if (async == Async::yes) | |||
saveProjectAsync (exporterToSave, std::move (onCompletion)); | |||
else | |||
onCompletion (saveProject (exporterToSave)); | |||
} | |||
void ProjectSaver::saveProjectAsync (ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion) | |||
{ | |||
jassert (saveThread == nullptr); | |||
WeakReference<ProjectSaver> ref (this); | |||
saveThread = std::make_unique<SaveThreadWithProgressWindow> (*this, exporterToSave, [ref, onCompletion] (Result result) | |||
{ | |||
SaveThreadWithProgressWindow thread (*this, exporterToSave); | |||
thread.runThread(); | |||
if (ref == nullptr) | |||
return; | |||
return thread.result; | |||
} | |||
// Clean up old save thread in case onCompletion wants to start a new save thread | |||
ref->saveThread->waitForThreadToExit (-1); | |||
ref->saveThread = nullptr; | |||
return saveProject (exporterToSave); | |||
if (onCompletion != nullptr) | |||
onCompletion (result); | |||
}); | |||
saveThread->launchThread(); | |||
} | |||
Result ProjectSaver::saveResourcesOnly() | |||
@@ -36,7 +36,7 @@ class ProjectSaver | |||
public: | |||
ProjectSaver (Project& projectToSave); | |||
Result save (ProjectExporter* exporterToSave = nullptr); | |||
void save (Async async, ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion); | |||
Result saveResourcesOnly(); | |||
void saveBasicProjectItems (const OwnedArray<LibraryModule>& modules, const String& appConfigUserContent); | |||
@@ -52,21 +52,30 @@ private: | |||
struct SaveThreadWithProgressWindow : public ThreadWithProgressWindow | |||
{ | |||
public: | |||
SaveThreadWithProgressWindow (ProjectSaver& ps, ProjectExporter* exporterToSave) | |||
SaveThreadWithProgressWindow (ProjectSaver& ps, | |||
ProjectExporter* exporterToSave, | |||
std::function<void (Result)> onCompletionIn) | |||
: ThreadWithProgressWindow ("Saving...", true, false), | |||
saver (ps), | |||
specifiedExporterToSave (exporterToSave) | |||
{} | |||
specifiedExporterToSave (exporterToSave), | |||
onCompletion (std::move (onCompletionIn)) | |||
{ | |||
jassert (onCompletion != nullptr); | |||
} | |||
void run() override | |||
{ | |||
setProgress (-1); | |||
result = saver.saveProject (specifiedExporterToSave); | |||
const auto result = saver.saveProject (specifiedExporterToSave); | |||
const auto callback = onCompletion; | |||
MessageManager::callAsync ([callback, result] { callback (result); }); | |||
} | |||
private: | |||
ProjectSaver& saver; | |||
Result result = Result::ok(); | |||
ProjectExporter* specifiedExporterToSave; | |||
std::function<void (Result)> onCompletion; | |||
JUCE_DECLARE_NON_COPYABLE (SaveThreadWithProgressWindow) | |||
}; | |||
@@ -86,6 +95,7 @@ private: | |||
OwnedArray<LibraryModule> getModules(); | |||
Result saveProject (ProjectExporter* specifiedExporterToSave); | |||
void saveProjectAsync (ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion); | |||
template <typename WriterCallback> | |||
void writeOrRemoveGeneratedFile (const String& name, WriterCallback&& writerCallback); | |||
@@ -118,8 +128,11 @@ private: | |||
CriticalSection errorLock; | |||
StringArray errors; | |||
std::unique_ptr<SaveThreadWithProgressWindow> saveThread; | |||
bool hasBinaryData = false; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectSaver) | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (ProjectSaver) | |||
}; |
@@ -60,16 +60,15 @@ namespace | |||
class NewCppFileWizard : public NewFileWizard::Type | |||
{ | |||
public: | |||
NewCppFileWizard() {} | |||
String getName() override { return "CPP File"; } | |||
void createNewFile (Project&, Project::Item parent) override | |||
{ | |||
const File newFile (askUserToChooseNewFile ("SourceCode.cpp", "*.cpp", parent)); | |||
if (newFile != File()) | |||
create (parent, newFile, "jucer_NewCppFileTemplate_cpp"); | |||
askUserToChooseNewFile ("SourceCode.cpp", "*.cpp", parent, [parent] (File newFile) | |||
{ | |||
if (newFile != File()) | |||
create (parent, newFile, "jucer_NewCppFileTemplate_cpp"); | |||
}); | |||
} | |||
static bool create (Project::Item parent, const File& newFile, const char* templateName) | |||
@@ -89,16 +88,15 @@ public: | |||
class NewHeaderFileWizard : public NewFileWizard::Type | |||
{ | |||
public: | |||
NewHeaderFileWizard() {} | |||
String getName() override { return "Header File"; } | |||
void createNewFile (Project&, Project::Item parent) override | |||
{ | |||
const File newFile (askUserToChooseNewFile ("SourceCode.h", "*.h", parent)); | |||
if (newFile != File()) | |||
create (parent, newFile, "jucer_NewCppFileTemplate_h"); | |||
askUserToChooseNewFile ("SourceCode.h", "*.h", parent, [parent] (File newFile) | |||
{ | |||
if (newFile != File()) | |||
create (parent, newFile, "jucer_NewCppFileTemplate_h"); | |||
}); | |||
} | |||
static bool create (Project::Item parent, const File& newFile, const char* templateName) | |||
@@ -118,19 +116,15 @@ public: | |||
class NewCppAndHeaderFileWizard : public NewFileWizard::Type | |||
{ | |||
public: | |||
NewCppAndHeaderFileWizard() {} | |||
String getName() override { return "CPP & Header File"; } | |||
void createNewFile (Project&, Project::Item parent) override | |||
{ | |||
const File newFile (askUserToChooseNewFile ("SourceCode.h", "*.h;*.cpp", parent)); | |||
if (newFile != File()) | |||
askUserToChooseNewFile ("SourceCode.h", "*.h;*.cpp", parent, [parent] (File newFile) | |||
{ | |||
if (NewCppFileWizard::create (parent, newFile.withFileExtension ("h"), "jucer_NewCppFileTemplate_h")) | |||
NewCppFileWizard::create (parent, newFile.withFileExtension ("cpp"), "jucer_NewCppFileTemplate_cpp"); | |||
} | |||
}); | |||
} | |||
}; | |||
@@ -138,37 +132,11 @@ public: | |||
class NewComponentFileWizard : public NewFileWizard::Type | |||
{ | |||
public: | |||
NewComponentFileWizard() {} | |||
String getName() override { return "Component class (split between a CPP & header)"; } | |||
void createNewFile (Project&, Project::Item parent) override | |||
{ | |||
for (;;) | |||
{ | |||
AlertWindow aw (TRANS ("Create new Component class"), | |||
TRANS ("Please enter the name for the new class"), | |||
AlertWindow::NoIcon, nullptr); | |||
aw.addTextEditor (getClassNameFieldName(), String(), String(), false); | |||
aw.addButton (TRANS ("Create Files"), 1, KeyPress (KeyPress::returnKey)); | |||
aw.addButton (TRANS ("Cancel"), 0, KeyPress (KeyPress::escapeKey)); | |||
if (aw.runModalLoop() == 0) | |||
break; | |||
const String className (aw.getTextEditorContents (getClassNameFieldName()).trim()); | |||
if (className == build_tools::makeValidIdentifier (className, false, true, false)) | |||
{ | |||
const File newFile (askUserToChooseNewFile (className + ".h", "*.h;*.cpp", parent)); | |||
if (newFile != File()) | |||
createFiles (parent, className, newFile); | |||
break; | |||
} | |||
} | |||
createNewFileInternal (parent); | |||
} | |||
static bool create (const String& className, Project::Item parent, | |||
@@ -198,14 +166,61 @@ private: | |||
} | |||
static String getClassNameFieldName() { return "Class Name"; } | |||
void createNewFileInternal (Project::Item parent) | |||
{ | |||
asyncAlertWindow = std::make_unique<AlertWindow> (TRANS ("Create new Component class"), | |||
TRANS ("Please enter the name for the new class"), | |||
AlertWindow::NoIcon, nullptr); | |||
asyncAlertWindow->addTextEditor (getClassNameFieldName(), String(), String(), false); | |||
asyncAlertWindow->addButton (TRANS ("Create Files"), 1, KeyPress (KeyPress::returnKey)); | |||
asyncAlertWindow->addButton (TRANS ("Cancel"), 0, KeyPress (KeyPress::escapeKey)); | |||
WeakReference<NewComponentFileWizard> safeThis { this }; | |||
asyncAlertWindow->enterModalState (true, ModalCallbackFunction::create ([safeThis, parent] (int result) | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
auto& aw = *(safeThis->asyncAlertWindow); | |||
aw.exitModalState (result); | |||
aw.setVisible (false); | |||
if (result == 0) | |||
return; | |||
const String className (aw.getTextEditorContents (getClassNameFieldName()).trim()); | |||
if (className == build_tools::makeValidIdentifier (className, false, true, false)) | |||
{ | |||
safeThis->askUserToChooseNewFile (className + ".h", "*.h;*.cpp", parent, [safeThis, parent, className] (File newFile) | |||
{ | |||
if (safeThis == nullptr) | |||
return; | |||
if (newFile != File()) | |||
safeThis->createFiles (parent, className, newFile); | |||
}); | |||
return; | |||
} | |||
safeThis->createNewFileInternal (parent); | |||
}), false); | |||
} | |||
std::unique_ptr<AlertWindow> asyncAlertWindow; | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (NewComponentFileWizard) | |||
}; | |||
//============================================================================== | |||
class NewSingleFileComponentFileWizard : public NewComponentFileWizard | |||
{ | |||
public: | |||
NewSingleFileComponentFileWizard() {} | |||
String getName() override { return "Component class (in a single source file)"; } | |||
void createFiles (Project::Item parent, const String& className, const File& newFile) override | |||
@@ -218,24 +233,28 @@ public: | |||
//============================================================================== | |||
void NewFileWizard::Type::showFailedToWriteMessage (const File& file) | |||
{ | |||
AlertWindow::showMessageBox (AlertWindow::WarningIcon, | |||
"Failed to Create File!", | |||
"Couldn't write to the file: " + file.getFullPathName()); | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
"Failed to Create File!", | |||
"Couldn't write to the file: " + file.getFullPathName()); | |||
} | |||
File NewFileWizard::Type::askUserToChooseNewFile (const String& suggestedFilename, const String& wildcard, | |||
const Project::Item& projectGroupToAddTo) | |||
void NewFileWizard::Type::askUserToChooseNewFile (const String& suggestedFilename, const String& wildcard, | |||
const Project::Item& projectGroupToAddTo, | |||
std::function<void (File)> callback) | |||
{ | |||
FileChooser fc ("Select File to Create", | |||
projectGroupToAddTo.determineGroupFolder() | |||
.getChildFile (suggestedFilename) | |||
.getNonexistentSibling(), | |||
wildcard); | |||
if (fc.browseForFileToSave (true)) | |||
return fc.getResult(); | |||
return {}; | |||
chooser = std::make_unique<FileChooser> ("Select File to Create", | |||
projectGroupToAddTo.determineGroupFolder() | |||
.getChildFile (suggestedFilename) | |||
.getNonexistentSibling(), | |||
wildcard); | |||
auto flags = FileBrowserComponent::saveMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::warnAboutOverwriting; | |||
chooser->launchAsync (flags, [callback] (const FileChooser& fc) | |||
{ | |||
callback (fc.getResult()); | |||
}); | |||
} | |||
//============================================================================== | |||
@@ -48,10 +48,14 @@ public: | |||
protected: | |||
//============================================================================== | |||
File askUserToChooseNewFile (const String& suggestedFilename, const String& wildcard, | |||
const Project::Item& projectGroupToAddTo); | |||
void askUserToChooseNewFile (const String& suggestedFilename, const String& wildcard, | |||
const Project::Item& projectGroupToAddTo, | |||
std::function<void (File)> callback); | |||
static void showFailedToWriteMessage (const File& file); | |||
private: | |||
std::unique_ptr<FileChooser> chooser; | |||
}; | |||
//============================================================================== | |||
@@ -134,17 +134,29 @@ private: | |||
if (isDirectory) | |||
{ | |||
FileChooser chooser ("Select directory", currentFile); | |||
chooser = std::make_unique<FileChooser> ("Select directory", currentFile); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (chooser.browseForDirectory()) | |||
setTo (chooser.getResult()); | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
setTo (fc.getResult()); | |||
}); | |||
} | |||
else | |||
{ | |||
FileChooser chooser ("Select file", currentFile, wildcards); | |||
chooser = std::make_unique<FileChooser> ("Select file", currentFile, wildcards); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; | |||
if (chooser.browseForFileToOpen()) | |||
setTo (chooser.getResult()); | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
setTo (fc.getResult()); | |||
}); | |||
} | |||
} | |||
@@ -189,6 +201,8 @@ private: | |||
String wildcards; | |||
File root; | |||
std::unique_ptr<FileChooser> chooser; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilePathPropertyComponent) | |||
}; | |||
@@ -191,11 +191,18 @@ public: | |||
/** Pops up a dialog letting the user save the processor's state to a file. */ | |||
void askUserToSaveState (const String& fileSuffix = String()) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser fc (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix)); | |||
if (fc.browseForFileToSave (true)) | |||
stateFileChooser = std::make_unique<FileChooser> (TRANS("Save current state"), | |||
getLastFile(), | |||
getFilePatterns (fileSuffix)); | |||
auto flags = FileBrowserComponent::saveMode | |||
| FileBrowserComponent::canSelectFiles | |||
| FileBrowserComponent::warnAboutOverwriting; | |||
stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
setLastFile (fc); | |||
MemoryBlock data; | |||
@@ -205,20 +212,23 @@ public: | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Error whilst saving"), | |||
TRANS("Couldn't write to the specified file!")); | |||
} | |||
#else | |||
ignoreUnused (fileSuffix); | |||
#endif | |||
}); | |||
} | |||
/** Pops up a dialog letting the user re-load the processor's state from a file. */ | |||
void askUserToLoadState (const String& fileSuffix = String()) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser fc (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix)); | |||
stateFileChooser = std::make_unique<FileChooser> (TRANS("Load a saved state"), | |||
getLastFile(), | |||
getFilePatterns (fileSuffix)); | |||
auto flags = FileBrowserComponent::openMode | |||
| FileBrowserComponent::canSelectFiles; | |||
if (fc.browseForFileToOpen()) | |||
stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
setLastFile (fc); | |||
MemoryBlock data; | |||
@@ -229,10 +239,7 @@ public: | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Error whilst loading"), | |||
TRANS("Couldn't read from the specified file!")); | |||
} | |||
#else | |||
ignoreUnused (fileSuffix); | |||
#endif | |||
}); | |||
} | |||
//============================================================================== | |||
@@ -407,6 +414,8 @@ public: | |||
std::unique_ptr<AudioDeviceManager::AudioDeviceSetup> options; | |||
Array<MidiDeviceInfo> lastMidiDevices; | |||
std::unique_ptr<FileChooser> stateFileChooser; | |||
private: | |||
/* This class can be used to ensure that audio callbacks use buffers with a | |||
predictable maximum size. | |||
@@ -335,7 +335,7 @@ namespace juce | |||
#elif ! defined (JUCE_MODAL_LOOPS_PERMITTED) | |||
/** Some operating environments don't provide a modal loop mechanism, so this flag can be | |||
used to disable any functions that try to run a modal loop. */ | |||
#define JUCE_MODAL_LOOPS_PERMITTED 1 | |||
#define JUCE_MODAL_LOOPS_PERMITTED 0 | |||
#endif | |||
//============================================================================== | |||
@@ -79,7 +79,7 @@ public: | |||
*/ | |||
bool hasStopMessageBeenSent() const noexcept { return quitMessagePosted.get() != 0; } | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Synchronously dispatches messages until a given time has elapsed. | |||
Returns false if a quit message has been posted by a call to stopDispatchLoop(), | |||
@@ -2020,7 +2020,7 @@ public: | |||
virtual void handleCommandMessage (int commandId); | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Runs a component modally, waiting until the loop terminates. | |||
This method first makes the component visible, brings it to the front and | |||
@@ -119,7 +119,7 @@ public: | |||
*/ | |||
bool cancelAllModalComponents(); | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Runs the event loop until the currently topmost modal component is dismissed, and | |||
returns the exit code for that component. | |||
*/ | |||
@@ -164,6 +164,7 @@ class JUCE_API ModalCallbackFunction | |||
public: | |||
/** This is a utility function to create a ModalComponentManager::Callback that will | |||
call a lambda function. | |||
The lambda that you supply must take an integer parameter, which is the result code that | |||
was returned when the modal component was dismissed. | |||
@@ -30,22 +30,18 @@ namespace juce | |||
/** | |||
Creates a dialog box to choose a file or directory to load or save. | |||
To use a FileChooser: | |||
- create one (as a local stack variable is the neatest way) | |||
- call one of its browseFor.. methods | |||
- if this returns true, the user has selected a file, so you can retrieve it | |||
with the getResult() method. | |||
e.g. @code | |||
void loadMooseFile() | |||
{ | |||
FileChooser myChooser ("Please select the moose you want to load...", | |||
File::getSpecialLocation (File::userHomeDirectory), | |||
"*.moose"); | |||
myChooser = std::make_unique<FileChooser> ("Please select the moose you want to load...", | |||
File::getSpecialLocation (File::userHomeDirectory), | |||
"*.moose"); | |||
auto folderChooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (myChooser.browseForFileToOpen()) | |||
myChooser->launchAsync (folderChooserFlags, [this] (const FileChooser& chooser) | |||
{ | |||
File mooseFile (myChooser.getResult()); | |||
File mooseFile (chooser.getResult()); | |||
loadMoose (mooseFile); | |||
} | |||
@@ -125,7 +121,7 @@ public: | |||
~FileChooser(); | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Shows a dialog box to choose a file to open. | |||
This will display the dialog box modally, using an "open file" mode, so that | |||
@@ -181,7 +177,6 @@ public: | |||
browseForFileToOpen() for more info about the behaviour of this method. | |||
*/ | |||
bool browseForMultipleFilesOrDirectories (FilePreviewComponent* previewComponent = nullptr); | |||
#endif | |||
//============================================================================== | |||
/** Runs a dialog box for the given set of option flags. | |||
@@ -193,6 +188,7 @@ public: | |||
@see FileBrowserComponent::FileChooserFlags | |||
*/ | |||
bool showDialog (int flags, FilePreviewComponent* previewComponent); | |||
#endif | |||
/** Use this method to launch the file browser window asynchronously. | |||
@@ -200,12 +196,9 @@ public: | |||
structure and will launch it modally, returning immediately. | |||
You must specify a callback which is called when the file browser is | |||
canceled or a file is selected. To abort the file selection, simply | |||
cancelled or a file is selected. To abort the file selection, simply | |||
delete the FileChooser object. | |||
You can use the ModalCallbackFunction::create method to wrap a lambda | |||
into a modal Callback object. | |||
You must ensure that the lifetime of the callback object is longer than | |||
the lifetime of the file-chooser. | |||
*/ | |||
@@ -33,29 +33,34 @@ namespace juce | |||
This is a Juce-based file dialog box; to use a native file chooser, see the | |||
FileChooser class. | |||
To use one of these, create it and call its show() method. e.g. | |||
@code | |||
{ | |||
WildcardFileFilter wildcardFilter ("*.foo", String(), "Foo files"); | |||
wildcardFilter = std::make_unique<WildcardFileFilter> ("*.foo", String(), "Foo files"); | |||
FileBrowserComponent browser (FileBrowserComponent::canSelectFiles, | |||
File(), | |||
&wildcardFilter, | |||
nullptr); | |||
browser = std::make_unique<FileBrowserComponent> (FileBrowserComponent::canSelectFiles, | |||
File(), | |||
wildcardFilter.get(), | |||
nullptr); | |||
FileChooserDialogBox dialogBox ("Open some kind of file", | |||
"Please choose some kind of file that you want to open...", | |||
browser, | |||
false, | |||
Colours::lightgrey); | |||
dialogBox = std::make_unique<FileChooserDialogBox> ("Open some kind of file", | |||
"Please choose some kind of file that you want to open...", | |||
*browser, | |||
false, | |||
Colours::lightgrey); | |||
if (dialogBox.show()) | |||
auto onFileSelected = [this] (int r) | |||
{ | |||
File selectedFile = browser.getSelectedFile (0); | |||
modalStateFinished (r); | |||
auto selectedFile = browser->getSelectedFile (0); | |||
...etc... | |||
}; | |||
...etc.. | |||
} | |||
dialogBox->centreWithDefaultSize (nullptr); | |||
dialogBox->enterModalState (true, | |||
ModalCallbackFunction::create (onFileSelected), | |||
true); | |||
} | |||
@endcode | |||
@@ -99,7 +104,7 @@ public: | |||
~FileChooserDialogBox() override; | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Displays and runs the dialog box modally. | |||
This will show the box with the specified size, returning true if the user | |||
@@ -149,18 +149,18 @@ void FileSearchPathListComponent::deleteKeyPressed (int row) | |||
void FileSearchPathListComponent::returnKeyPressed (int row) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser chooser (TRANS("Change folder..."), path[row], "*"); | |||
chooser = std::make_unique<FileChooser> (TRANS("Change folder..."), path[row], "*"); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (chooser.browseForDirectory()) | |||
chooser->launchAsync (chooserFlags, [this, row] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
path.remove (row); | |||
path.add (chooser.getResult(), row); | |||
path.add (fc.getResult(), row); | |||
changed(); | |||
} | |||
#else | |||
ignoreUnused (row); | |||
#endif | |||
}); | |||
} | |||
void FileSearchPathListComponent::listBoxItemDoubleClicked (int row, const MouseEvent&) | |||
@@ -226,16 +226,17 @@ void FileSearchPathListComponent::addPath() | |||
if (start == File()) | |||
start = File::getCurrentWorkingDirectory(); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser chooser (TRANS("Add a folder..."), start, "*"); | |||
chooser = std::make_unique<FileChooser> (TRANS("Add a folder..."), start, "*"); | |||
auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories; | |||
if (chooser.browseForDirectory()) | |||
path.add (chooser.getResult(), listBox.getSelectedRow()); | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser& fc) | |||
{ | |||
if (fc.getResult() == File{}) | |||
return; | |||
changed(); | |||
#else | |||
jassertfalse; // needs rewriting to deal with non-modal environments | |||
#endif | |||
path.add (fc.getResult(), listBox.getSelectedRow()); | |||
changed(); | |||
}); | |||
} | |||
void FileSearchPathListComponent::deleteSelected() | |||
@@ -100,6 +100,7 @@ private: | |||
//============================================================================== | |||
FileSearchPath path; | |||
File defaultBrowseTarget; | |||
std::unique_ptr<FileChooser> chooser; | |||
ListBox listBox; | |||
TextButton addButton, removeButton, changeButton; | |||
@@ -114,22 +114,22 @@ File FilenameComponent::getLocationToBrowse() | |||
void FilenameComponent::showChooser() | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser fc (isDir ? TRANS ("Choose a new directory") | |||
: TRANS ("Choose a new file"), | |||
getLocationToBrowse(), | |||
wildcard); | |||
if (isDir ? fc.browseForDirectory() | |||
: (isSaving ? fc.browseForFileToSave (false) | |||
: fc.browseForFileToOpen())) | |||
chooser = std::make_unique<FileChooser> (isDir ? TRANS ("Choose a new directory") | |||
: TRANS ("Choose a new file"), | |||
getLocationToBrowse(), | |||
wildcard); | |||
auto chooserFlags = isDir ? FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories | |||
: FileBrowserComponent::canSelectFiles | (isSaving ? FileBrowserComponent::saveMode | |||
: FileBrowserComponent::openMode); | |||
chooser->launchAsync (chooserFlags, [this] (const FileChooser&) | |||
{ | |||
setCurrentFile (fc.getResult(), true); | |||
} | |||
#else | |||
ignoreUnused (isSaving); | |||
jassertfalse; // needs rewriting to deal with non-modal environments | |||
#endif | |||
if (chooser->getResult() == File{}) | |||
return; | |||
setCurrentFile (chooser->getResult(), true); | |||
}); | |||
} | |||
bool FilenameComponent::isInterestedInFileDrag (const StringArray&) | |||
@@ -73,21 +73,21 @@ public: | |||
//============================================================================== | |||
/** Creates a FilenameComponent. | |||
@param name the name for this component. | |||
@param currentFile the file to initially show in the box | |||
@param canEditFilename if true, the user can manually edit the filename; if false, | |||
they can only change it by browsing for a new file | |||
@param isDirectory if true, the file will be treated as a directory, and | |||
an appropriate directory browser used | |||
@param isForSaving if true, the file browser will allow non-existent files to | |||
be picked, as the file is assumed to be used for saving rather | |||
than loading | |||
@param fileBrowserWildcard a wildcard pattern to use in the file browser - e.g. "*.txt;*.foo". | |||
If an empty string is passed in, then the pattern is assumed to be "*" | |||
@param enforcedSuffix if this is non-empty, it is treated as a suffix that will be added | |||
to any filenames that are entered or chosen | |||
@param name the name for this component. | |||
@param currentFile the file to initially show in the box | |||
@param canEditFilename if true, the user can manually edit the filename; if false, | |||
they can only change it by browsing for a new file | |||
@param isDirectory if true, the file will be treated as a directory, and | |||
an appropriate directory browser used | |||
@param isForSaving if true, the file browser will allow non-existent files to | |||
be picked, as the file is assumed to be used for saving rather | |||
than loading | |||
@param fileBrowserWildcard a wildcard pattern to use in the file browser - e.g. "*.txt;*.foo". | |||
If an empty string is passed in, then the pattern is assumed to be "*" | |||
@param enforcedSuffix if this is non-empty, it is treated as a suffix that will be added | |||
to any filenames that are entered or chosen | |||
@param textWhenNothingSelected the message to display in the box before any filename is entered. (This | |||
will only appear if the initial file isn't valid) | |||
will only appear if the initial file isn't valid) | |||
*/ | |||
FilenameComponent (const String& name, | |||
const File& currentFile, | |||
@@ -216,6 +216,10 @@ public: | |||
private: | |||
//============================================================================== | |||
void handleAsyncUpdate() override; | |||
void showChooser(); | |||
ComboBox filenameBox; | |||
String lastFilename; | |||
std::unique_ptr<Button> browseButton; | |||
@@ -224,9 +228,7 @@ private: | |||
String wildcard, enforcedSuffix, browseButtonText; | |||
ListenerList <FilenameComponentListener> listeners; | |||
File defaultBrowseFile; | |||
void showChooser(); | |||
void handleAsyncUpdate() override; | |||
std::unique_ptr<FileChooser> chooser; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilenameComponent) | |||
}; | |||
@@ -45,7 +45,6 @@ public: | |||
/** Destructor. */ | |||
~ImagePreviewComponent() override; | |||
//============================================================================== | |||
/** @internal */ | |||
void selectedFileChanged (const File& newSelectedFile) override; | |||
@@ -48,7 +48,7 @@ void MultiDocumentPanelWindow::maximiseButtonPressed() | |||
void MultiDocumentPanelWindow::closeButtonPressed() | |||
{ | |||
if (auto* owner = getOwner()) | |||
owner->closeDocument (getContentComponent(), true); | |||
owner->closeDocumentAsync (getContentComponent(), true, nullptr); | |||
else | |||
jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel! | |||
} | |||
@@ -95,10 +95,7 @@ MultiDocumentPanel::MultiDocumentPanel() | |||
setOpaque (true); | |||
} | |||
MultiDocumentPanel::~MultiDocumentPanel() | |||
{ | |||
closeAllDocuments (false); | |||
} | |||
MultiDocumentPanel::~MultiDocumentPanel() = default; | |||
//============================================================================== | |||
namespace MultiDocHelpers | |||
@@ -109,6 +106,7 @@ namespace MultiDocHelpers | |||
} | |||
} | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst) | |||
{ | |||
while (! components.isEmpty()) | |||
@@ -117,6 +115,52 @@ bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst) | |||
return true; | |||
} | |||
#endif | |||
void MultiDocumentPanel::closeLastDocumentRecursive (SafePointer<MultiDocumentPanel> parent, | |||
bool checkItsOkToCloseFirst, | |||
std::function<void (bool)> callback) | |||
{ | |||
if (parent->components.isEmpty()) | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
parent->closeDocumentAsync (parent->components.getLast(), | |||
checkItsOkToCloseFirst, | |||
[parent, checkItsOkToCloseFirst, callback] (bool closeResult) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (! closeResult) | |||
{ | |||
if (callback != nullptr) | |||
callback (false); | |||
return; | |||
} | |||
parent->closeLastDocumentRecursive (parent, checkItsOkToCloseFirst, std::move (callback)); | |||
}); | |||
} | |||
void MultiDocumentPanel::closeAllDocumentsAsync (bool checkItsOkToCloseFirst, std::function<void (bool)> callback) | |||
{ | |||
closeLastDocumentRecursive (this, checkItsOkToCloseFirst, std::move (callback)); | |||
} | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
bool MultiDocumentPanel::tryToCloseDocument (Component*) | |||
{ | |||
// If you hit this assertion then you need to implement this method in a subclass. | |||
jassertfalse; | |||
return false; | |||
} | |||
#endif | |||
MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow() | |||
{ | |||
@@ -217,93 +261,105 @@ bool MultiDocumentPanel::addDocument (Component* const component, | |||
return true; | |||
} | |||
bool MultiDocumentPanel::closeDocument (Component* component, | |||
const bool checkItsOkToCloseFirst) | |||
void MultiDocumentPanel::closeDocumentInternal (Component* component) | |||
{ | |||
// Intellisense warns about component being uninitialised. | |||
// I'm not sure how a function argument could be uninitialised. | |||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001) | |||
if (component == nullptr) | |||
return true; | |||
component->removeComponentListener (this); | |||
if (components.contains (component)) | |||
{ | |||
if (checkItsOkToCloseFirst && ! tryToCloseDocument (component)) | |||
return false; | |||
const bool shouldDelete = MultiDocHelpers::shouldDeleteComp (component); | |||
component->getProperties().remove ("mdiDocumentDelete_"); | |||
component->getProperties().remove ("mdiDocumentBkg_"); | |||
component->removeComponentListener (this); | |||
const bool shouldDelete = MultiDocHelpers::shouldDeleteComp (component); | |||
component->getProperties().remove ("mdiDocumentDelete_"); | |||
component->getProperties().remove ("mdiDocumentBkg_"); | |||
if (mode == FloatingWindows) | |||
if (mode == FloatingWindows) | |||
{ | |||
for (auto* child : getChildren()) | |||
{ | |||
for (auto* child : getChildren()) | |||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child)) | |||
{ | |||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child)) | |||
if (dw->getContentComponent() == component) | |||
{ | |||
if (dw->getContentComponent() == component) | |||
{ | |||
std::unique_ptr<MultiDocumentPanelWindow> (dw)->clearContentComponent(); | |||
break; | |||
} | |||
std::unique_ptr<MultiDocumentPanelWindow> (dw)->clearContentComponent(); | |||
break; | |||
} | |||
} | |||
} | |||
if (shouldDelete) | |||
delete component; | |||
if (shouldDelete) | |||
delete component; | |||
components.removeFirstMatchingValue (component); | |||
components.removeFirstMatchingValue (component); | |||
if (isFullscreenWhenOneDocument() && components.size() == 1) | |||
if (isFullscreenWhenOneDocument() && components.size() == 1) | |||
{ | |||
for (int i = getNumChildComponents(); --i >= 0;) | |||
{ | |||
for (int i = getNumChildComponents(); --i >= 0;) | |||
{ | |||
std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i))); | |||
if (dw != nullptr) | |||
dw->clearContentComponent(); | |||
} | |||
std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i))); | |||
addAndMakeVisible (components.getFirst()); | |||
if (dw != nullptr) | |||
dw->clearContentComponent(); | |||
} | |||
addAndMakeVisible (components.getFirst()); | |||
} | |||
} | |||
else | |||
{ | |||
jassert (components.indexOf (component) >= 0); | |||
if (tabComponent != nullptr) | |||
{ | |||
for (int i = tabComponent->getNumTabs(); --i >= 0;) | |||
if (tabComponent->getTabContentComponent (i) == component) | |||
tabComponent->removeTab (i); | |||
} | |||
else | |||
{ | |||
jassert (components.indexOf (component) >= 0); | |||
removeChildComponent (component); | |||
} | |||
if (tabComponent != nullptr) | |||
{ | |||
for (int i = tabComponent->getNumTabs(); --i >= 0;) | |||
if (tabComponent->getTabContentComponent (i) == component) | |||
tabComponent->removeTab (i); | |||
} | |||
else | |||
{ | |||
removeChildComponent (component); | |||
} | |||
if (shouldDelete) | |||
delete component; | |||
if (shouldDelete) | |||
delete component; | |||
if (tabComponent != nullptr && tabComponent->getNumTabs() <= numDocsBeforeTabsUsed) | |||
tabComponent.reset(); | |||
if (tabComponent != nullptr && tabComponent->getNumTabs() <= numDocsBeforeTabsUsed) | |||
tabComponent.reset(); | |||
components.removeFirstMatchingValue (component); | |||
components.removeFirstMatchingValue (component); | |||
if (components.size() > 0 && tabComponent == nullptr) | |||
addAndMakeVisible (components.getFirst()); | |||
} | |||
if (components.size() > 0 && tabComponent == nullptr) | |||
addAndMakeVisible (components.getFirst()); | |||
} | |||
resized(); | |||
resized(); | |||
// This ensures that the active tab is painted properly when a tab is closed! | |||
if (auto* activeComponent = getActiveDocument()) | |||
setActiveDocument (activeComponent); | |||
// This ensures that the active tab is painted properly when a tab is closed! | |||
if (auto* activeComponent = getActiveDocument()) | |||
setActiveDocument (activeComponent); | |||
activeDocumentChanged(); | |||
activeDocumentChanged(); | |||
JUCE_END_IGNORE_WARNINGS_MSVC | |||
} | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
bool MultiDocumentPanel::closeDocument (Component* component, | |||
const bool checkItsOkToCloseFirst) | |||
{ | |||
// Intellisense warns about component being uninitialised. | |||
// I'm not sure how a function argument could be uninitialised. | |||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001) | |||
if (component == nullptr) | |||
return true; | |||
if (components.contains (component)) | |||
{ | |||
if (checkItsOkToCloseFirst && ! tryToCloseDocument (component)) | |||
return false; | |||
closeDocumentInternal (component); | |||
} | |||
else | |||
{ | |||
@@ -314,6 +370,54 @@ bool MultiDocumentPanel::closeDocument (Component* component, | |||
JUCE_END_IGNORE_WARNINGS_MSVC | |||
} | |||
#endif | |||
void MultiDocumentPanel::closeDocumentAsync (Component* component, | |||
const bool checkItsOkToCloseFirst, | |||
std::function<void (bool)> callback) | |||
{ | |||
// Intellisense warns about component being uninitialised. | |||
// I'm not sure how a function argument could be uninitialised. | |||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001) | |||
if (component == nullptr) | |||
{ | |||
if (callback != nullptr) | |||
callback (true); | |||
return; | |||
} | |||
if (components.contains (component)) | |||
{ | |||
if (checkItsOkToCloseFirst) | |||
{ | |||
SafePointer<MultiDocumentPanel> parent { this }; | |||
tryToCloseDocumentAsync (component, [parent, component, callback] (bool closedSuccessfully) | |||
{ | |||
if (parent == nullptr) | |||
return; | |||
if (closedSuccessfully) | |||
parent->closeDocumentInternal (component); | |||
if (callback != nullptr) | |||
callback (closedSuccessfully); | |||
}); | |||
return; | |||
} | |||
} | |||
else | |||
{ | |||
jassertfalse; | |||
} | |||
if (callback != nullptr) | |||
callback (true); | |||
JUCE_END_IGNORE_WARNINGS_MSVC | |||
} | |||
int MultiDocumentPanel::getNumDocuments() const noexcept | |||
{ | |||
@@ -108,6 +108,7 @@ public: | |||
~MultiDocumentPanel() override; | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Tries to close all the documents. | |||
If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | |||
@@ -120,6 +121,22 @@ public: | |||
@see closeDocument | |||
*/ | |||
bool closeAllDocuments (bool checkItsOkToCloseFirst); | |||
#endif | |||
/** Tries to close all the documents. | |||
If checkItsOkToCloseFirst is true, then the tryToCloseDocumentAsync() method will | |||
be called for each open document, and any of these calls fails, this method | |||
will stop and provide an argument of false to the callback, leaving some documents | |||
still open. | |||
If checkItsOkToCloseFirst is false, then all documents will be closed | |||
unconditionally. | |||
@see closeDocument | |||
*/ | |||
void closeAllDocumentsAsync (bool checkItsOkToCloseFirst, | |||
std::function<void (bool)> callback); | |||
/** Adds a document component to the panel. | |||
@@ -142,6 +159,7 @@ public: | |||
Colour backgroundColour, | |||
bool deleteWhenRemoved); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Closes one of the documents. | |||
If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | |||
@@ -158,6 +176,25 @@ public: | |||
*/ | |||
bool closeDocument (Component* component, | |||
bool checkItsOkToCloseFirst); | |||
#endif | |||
/** Closes one of the documents. | |||
If checkItsOkToCloseFirst is true, then the tryToCloseDocumentAsync() method will | |||
be called, and if it fails, this method will call the callback with a false | |||
argument without closing the document. | |||
If checkItsOkToCloseFirst is false, then the documents will be closed | |||
unconditionally. | |||
The component will be deleted if the deleteWhenRemoved parameter was set to | |||
true when it was added with addDocument. | |||
@see addDocument, closeAllDocuments | |||
*/ | |||
void closeDocumentAsync (Component* component, | |||
bool checkItsOkToCloseFirst, | |||
std::function<void (bool)> callback); | |||
/** Returns the number of open document windows. | |||
@@ -248,6 +285,7 @@ public: | |||
TabbedComponent* getCurrentTabbedComponent() const noexcept { return tabComponent.get(); } | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** A subclass must override this to say whether its currently ok for a document | |||
to be closed. | |||
@@ -269,7 +307,32 @@ public: | |||
@see closeDocument, FileBasedDocument::saveIfNeededAndUserAgrees() | |||
*/ | |||
virtual bool tryToCloseDocument (Component* component) = 0; | |||
virtual bool tryToCloseDocument (Component* component); | |||
#endif | |||
/** A subclass must override this to say whether its currently ok for a document | |||
to be closed. | |||
This method is called by closeDocumentAsync() and closeAllDocumentsAsync() | |||
to indicate that a document should be saved if possible, ready for it to be closed. | |||
If the callback is called with a true argument, then it means the document is ok | |||
and can be closed. | |||
If the callback is called with a false argument, then it means that the | |||
closeDocumentAsync() method should stop and not close. | |||
Normally, you'd use this method to ask the user if they want to save any changes, | |||
then return true if the save operation went ok. If the user cancelled the save | |||
operation you could give a value of false to the callback to abort the close operation. | |||
If your component is based on the FileBasedDocument class, then you'd probably want | |||
to call FileBasedDocument::saveIfNeededAndUserAgreesAsync() and call the calback with | |||
true if this returned FileBasedDocument::savedOk. | |||
@see closeDocumentAsync, FileBasedDocument::saveIfNeededAndUserAgreesAsync() | |||
*/ | |||
virtual void tryToCloseDocumentAsync (Component* component, std::function<void (bool)> callback) = 0; | |||
/** Creates a new window to be used for a document. | |||
@@ -288,12 +351,12 @@ public: | |||
private: | |||
//============================================================================== | |||
LayoutMode mode = MaximisedWindowsWithTabs; | |||
Array<Component*> components; | |||
std::unique_ptr<TabbedComponent> tabComponent; | |||
Colour backgroundColour { Colours::lightblue }; | |||
int maximumNumDocuments = 0, numDocsBeforeTabsUsed = 0; | |||
void closeDocumentInternal (Component*); | |||
static void closeLastDocumentRecursive (SafePointer<MultiDocumentPanel>, | |||
bool, | |||
std::function<void (bool)>); | |||
//============================================================================== | |||
struct TabbedComponentInternal; | |||
friend class MultiDocumentPanelWindow; | |||
@@ -301,6 +364,12 @@ private: | |||
void updateOrder(); | |||
void addWindow (Component*); | |||
LayoutMode mode = MaximisedWindowsWithTabs; | |||
Array<Component*> components; | |||
std::unique_ptr<TabbedComponent> tabComponent; | |||
Colour backgroundColour { Colours::lightblue }; | |||
int maximumNumDocuments = 0, numDocsBeforeTabsUsed = 0; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiDocumentPanel) | |||
}; | |||
@@ -497,7 +497,7 @@ public: | |||
}; | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Displays the menu and waits for the user to pick something. | |||
This will display the menu modally, and return the ID of the item that the | |||
@@ -228,7 +228,7 @@ public: | |||
//============================================================================== | |||
// easy-to-use message box functions: | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Shows a dialog box that just has a message and a single button to get rid of it. | |||
The box is shown modally, and the method will block until the user has clicked the | |||
@@ -394,7 +394,7 @@ public: | |||
it'll show a box with just an ok button | |||
@returns true if the ok button was pressed, false if they pressed cancel. | |||
*/ | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
static bool JUCE_CALLTYPE showNativeDialogBox (const String& title, | |||
const String& bodyText, | |||
bool isOkCancel); | |||
@@ -142,7 +142,7 @@ public: | |||
*/ | |||
DialogWindow* create(); | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Launches and runs the dialog modally, returning the status code that was | |||
used to terminate the modal loop. | |||
@@ -201,7 +201,7 @@ public: | |||
bool shouldBeResizable = false, | |||
bool useBottomRightCornerResizer = false); | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Easy way of quickly showing a dialog box containing a given component. | |||
Note: This method has been superseded by the DialogWindow::LaunchOptions structure, | |||
@@ -47,7 +47,7 @@ public: | |||
alert window should be associated with. Depending on the look | |||
and feel, this might be used for positioning of the alert window. | |||
*/ | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
static void JUCE_CALLTYPE showMessageBox (AlertWindow::AlertIconType iconType, | |||
const String& title, | |||
const String& message, | |||
@@ -113,7 +113,7 @@ public: | |||
~ThreadWithProgressWindow() override; | |||
//============================================================================== | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Starts the thread and waits for it to finish. | |||
This will start the thread, make the dialog box appear, and wait until either | |||
@@ -72,7 +72,7 @@ public: | |||
@see setChangedFlag, changed | |||
*/ | |||
bool hasChangedSinceSaved() const { return changedSinceSave; } | |||
bool hasChangedSinceSaved() const; | |||
/** Called to indicate that the document has changed and needs saving. | |||
@@ -98,7 +98,7 @@ public: | |||
//============================================================================== | |||
/** Tries to open a file. | |||
If the file opens correctly, the document's file (see the getFile() method) is set | |||
If the file opens correctly the document's file (see the getFile() method) is set | |||
to this new one; if it fails, the document's file is left unchanged, and optionally | |||
a message box is shown telling the user there was an error. | |||
@@ -110,6 +110,22 @@ public: | |||
bool showMessageOnFailure, | |||
bool showWaitCursor = true); | |||
/** Tries to open a file. | |||
The callback is called with the result indicating whether the new file loaded | |||
successfully, or the error message if it failed. | |||
If the file opens correctly the document's file (see the getFile() method) is set | |||
to this new one; if it fails, the document's file is left unchanged, and optionally | |||
a message box is shown telling the user there was an error. | |||
@see loadDocumentAsync, loadFromUserSpecifiedFileAsync | |||
*/ | |||
void loadFromAsync (const File& fileToLoadFrom, | |||
bool showMessageOnFailure, | |||
std::function<void (Result)> callback); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Asks the user for a file and tries to load it. | |||
This will pop up a dialog box using the title, file extension and | |||
@@ -122,6 +138,19 @@ public: | |||
@see loadFrom | |||
*/ | |||
Result loadFromUserSpecifiedFile (bool showMessageOnFailure); | |||
#endif | |||
/** Asks the user for a file and tries to load it. | |||
This will pop up a dialog box using the title, file extension and | |||
wildcard specified in the document's constructor, and asks the user | |||
for a file. If they pick one, the loadFrom() method is used to | |||
try to load it, optionally showing a message if it fails. The result | |||
of the operation is provided in the callback function. | |||
@see loadFrom | |||
*/ | |||
void loadFromUserSpecifiedFileAsync (bool showMessageOnFailure, std::function<void (Result)> callback); | |||
//============================================================================== | |||
/** A set of possible outcomes of one of the save() methods | |||
@@ -133,6 +162,7 @@ public: | |||
failedToWriteToFile /**< indicates that it tried to write to a file but this failed. */ | |||
}; | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Tries to save the document to the last file it was saved or loaded from. | |||
This will always try to write to the file, even if the document isn't flagged as | |||
@@ -147,7 +177,26 @@ public: | |||
*/ | |||
SaveResult save (bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure); | |||
#endif | |||
/** Tries to save the document to the last file it was saved or loaded from. | |||
This will always try to write to the file, even if the document isn't flagged as | |||
having changed. | |||
@param askUserForFileIfNotSpecified if there's no file currently specified and this is | |||
true, it will prompt the user to pick a file, as if | |||
saveAsInteractive() was called. | |||
@param showMessageOnFailure if true it will show a warning message when if the | |||
save operation fails | |||
@param callback called after the save operation with the result | |||
@see saveIfNeededAndUserAgrees, saveAs, saveAsInteractive | |||
*/ | |||
void saveAsync (bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure, | |||
std::function<void (SaveResult)> callback); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** If the file needs saving, it'll ask the user if that's what they want to do, and save | |||
it if they say yes. | |||
@@ -169,7 +218,31 @@ public: | |||
@see save, saveAs, saveAsInteractive | |||
*/ | |||
SaveResult saveIfNeededAndUserAgrees(); | |||
#endif | |||
/** If the file needs saving, it'll ask the user if that's what they want to do, and save | |||
it if they say yes. | |||
If you've got a document open and want to close it (e.g. to quit the app), this is the | |||
method to call. | |||
If the document doesn't need saving the callback will be called with the value savedOk | |||
so you can go ahead and delete the document. | |||
If it does need saving it'll prompt the user, and if they say "discard changes" the | |||
callback will be called with savedOk, so again, you can safely delete the document. | |||
If the user clicks "cancel", the callback will be aclled with userCancelledSave, so | |||
you can abort the close-document operation. | |||
And if they click "save changes", it'll try to save and the callback will be called | |||
with either savedOk, or failedToWriteToFile if there was a problem. | |||
@see saveAsync, saveAsAsync, saveAsInteractiveAsync | |||
*/ | |||
void saveIfNeededAndUserAgreesAsync (std::function<void (SaveResult)> callback); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Tries to save the document to a specified file. | |||
If this succeeds, it'll also change the document's internal file (as returned by | |||
@@ -192,7 +265,32 @@ public: | |||
bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure, | |||
bool showWaitCursor = true); | |||
#endif | |||
/** Tries to save the document to a specified file. | |||
If this succeeds, it'll also change the document's internal file (as returned by | |||
the getFile() method). If it fails, the file will be left unchanged. | |||
@param newFile the file to try to write to | |||
@param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask the user | |||
first if they want to overwrite it | |||
@param askUserForFileIfNotSpecified if the file is non-existent and this is true, it'll | |||
use the saveAsInteractive() method to ask the user | |||
for a filename | |||
@param showMessageOnFailure if true and the write operation fails, it'll show | |||
a message box to warn the user | |||
@param callback called with the result of the save operation | |||
@see saveIfNeededAndUserAgreesAsync, saveAsync, saveAsInteractiveAsync | |||
*/ | |||
void saveAsAsync (const File& newFile, | |||
bool warnAboutOverwritingExistingFiles, | |||
bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure, | |||
std::function<void (SaveResult)> callback); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** Prompts the user for a filename and tries to save to it. | |||
This will pop up a dialog box using the title, file extension and | |||
@@ -201,10 +299,26 @@ public: | |||
to this file. | |||
@param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask | |||
the user first if they want to overwrite it | |||
the user first if they want to overwrite it | |||
@see saveIfNeededAndUserAgrees, save, saveAs | |||
*/ | |||
SaveResult saveAsInteractive (bool warnAboutOverwritingExistingFiles); | |||
#endif | |||
/** Prompts the user for a filename and tries to save to it. | |||
This will pop up a dialog box using the title, file extension and | |||
wildcard specified in the document's constructor, and asks the user | |||
for a file. If they pick one, the saveAs() method is used to try to save | |||
to this file. | |||
@param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask | |||
the user first if they want to overwrite it | |||
@param callback called with the result of the save operation | |||
@see saveIfNeededAndUserAgreesAsync, saveAsync, saveAsAsync | |||
*/ | |||
void saveAsInteractiveAsync (bool warnAboutOverwritingExistingFiles, | |||
std::function<void (SaveResult)> callback); | |||
//============================================================================== | |||
/** Returns the file that this document was last successfully saved or loaded from. | |||
@@ -214,7 +328,7 @@ public: | |||
It is changed when one of the load or save methods is used, or when setFile() | |||
is used to explicitly set it. | |||
*/ | |||
const File& getFile() const { return documentFile; } | |||
const File& getFile() const; | |||
/** Sets the file that this document thinks it was loaded from. | |||
@@ -224,7 +338,6 @@ public: | |||
*/ | |||
void setFile (const File& newFile); | |||
protected: | |||
//============================================================================== | |||
/** Overload this to return the title of the document. | |||
@@ -239,11 +352,33 @@ protected: | |||
*/ | |||
virtual Result loadDocument (const File& file) = 0; | |||
/** This method should try to load your document from the given file, then | |||
call the provided callback on the message thread, passing the result of the load. | |||
By default, this will synchronously call through to loadDocument. | |||
For longer-running load operations, you may wish to override this function to | |||
run the load on a background thread, and then to call the callback later on the | |||
message thread to signal that the load has completed. | |||
*/ | |||
virtual void loadDocumentAsync (const File& file, std::function<void (Result)> callback); | |||
/** This method should try to write your document to the given file. | |||
@returns a Result object to indicate the whether there was an error. | |||
*/ | |||
virtual Result saveDocument (const File& file) = 0; | |||
/** This method should try to write your document to the given file, then | |||
call the provided callback on the message thread, passing the result of the write. | |||
By default, this will synchronously call through to saveDocument. | |||
For longer-running save operations, you may wish to override this function to | |||
run the save on a background thread, and then to call the callback later on the | |||
message thread to signal that the save has completed. | |||
*/ | |||
virtual void saveDocumentAsync (const File& file, std::function<void (Result)> callback); | |||
/** This is used for dialog boxes to make them open at the last folder you | |||
were using. | |||
@@ -277,21 +412,18 @@ protected: | |||
*/ | |||
virtual void setLastDocumentOpened (const File& file) = 0; | |||
#if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||
/** This is called by saveAsInteractive() to allow you to optionally customise the | |||
/** This is called by saveAsInteractiveAsync() to allow you to optionally customise the | |||
filename that the user is presented with in the save dialog. | |||
The defaultFile parameter is an initial suggestion based on what the class knows | |||
about the current document - you can return a variation on this file with a different | |||
extension, etc, or just return something completely different. | |||
*/ | |||
virtual File getSuggestedSaveAsFile (const File& defaultFile); | |||
#endif | |||
private: | |||
//============================================================================== | |||
File documentFile; | |||
bool changedSinceSave = false; | |||
String fileExtension, fileWildcard, openFileDialogTitle, saveFileDialogTitle; | |||
class Pimpl; | |||
std::unique_ptr<Pimpl> pimpl; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileBasedDocument) | |||
}; | |||
@@ -317,7 +317,7 @@ public: | |||
GroupAlertBehaviour groupAlertBehaviour = alertAll; | |||
int timeoutAfterMs = 0; /**< specifies a duration in milliseconds, after which the notification should be | |||
cancelled, if it is not already canceled. Available from Android API 26 or above. */ | |||
cancelled, if it is not already cancelled. Available from Android API 26 or above. */ | |||
/**@}*/ | |||
}; | |||
@@ -95,17 +95,11 @@ struct WebViewKeyEquivalentResponder : public WebViewBase | |||
WebViewKeyEquivalentResponder() | |||
: WebViewBase ("WebViewKeyEquivalentResponder_") | |||
{ | |||
addIvar<WebViewKeyEquivalentResponder*> ("owner"); | |||
addMethod (@selector (performKeyEquivalent:), performKeyEquivalent, @encode (BOOL), "@:@"); | |||
registerClass(); | |||
} | |||
private: | |||
static WebViewKeyEquivalentResponder* getOwner (id self) | |||
{ | |||
return getIvar<WebViewKeyEquivalentResponder*> (self, "owner"); | |||
} | |||
static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) | |||
{ | |||
NSResponder* first = [[self window] firstResponder]; | |||
@@ -225,9 +219,42 @@ private: | |||
static void runOpenPanel (id, SEL, WKWebView*, WKOpenPanelParameters* parameters, WKFrameInfo*, | |||
void (^completionHandler)(NSArray<NSURL*>*)) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser chooser (TRANS("Select the file you want to upload..."), | |||
File::getSpecialLocation (File::userHomeDirectory), "*"); | |||
using CompletionHandlerType = decltype (completionHandler); | |||
class DeletedFileChooserWrapper : private DeletedAtShutdown | |||
{ | |||
public: | |||
DeletedFileChooserWrapper (std::unique_ptr<FileChooser> fc, CompletionHandlerType h) | |||
: chooser (std::move (fc)), handler (h) | |||
{ | |||
[handler retain]; | |||
} | |||
~DeletedFileChooserWrapper() | |||
{ | |||
callHandler (nullptr); | |||
[handler release]; | |||
} | |||
void callHandler (NSArray<NSURL*>* urls) | |||
{ | |||
if (handlerCalled) | |||
return; | |||
handler (urls); | |||
handlerCalled = true; | |||
} | |||
std::unique_ptr<FileChooser> chooser; | |||
private: | |||
CompletionHandlerType handler; | |||
bool handlerCalled = false; | |||
}; | |||
auto chooser = std::make_unique<FileChooser> (TRANS("Select the file you want to upload..."), | |||
File::getSpecialLocation (File::userHomeDirectory), "*"); | |||
auto* wrapper = new DeletedFileChooserWrapper (std::move (chooser), completionHandler); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | |||
| ([parameters allowsMultipleSelection] ? FileBrowserComponent::canSelectMultipleItems : 0); | |||
@@ -237,24 +264,17 @@ private: | |||
flags |= FileBrowserComponent::canSelectDirectories; | |||
#endif | |||
if (chooser.showDialog (flags, nullptr)) | |||
wrapper->chooser->launchAsync (flags, [wrapper] (const FileChooser&) | |||
{ | |||
auto results = chooser.getResults(); | |||
auto results = wrapper->chooser->getResults(); | |||
auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) results.size()]; | |||
for (auto& f : results) | |||
[urls addObject: [NSURL fileURLWithPath: juceStringToNS (f.getFullPathName())]]; | |||
completionHandler (urls); | |||
} | |||
else | |||
{ | |||
completionHandler (nil); | |||
} | |||
#else | |||
ignoreUnused (parameters, completionHandler); | |||
jassertfalse; // Can't use this without modal loops being enabled! | |||
#endif | |||
wrapper->callHandler (urls); | |||
delete wrapper; | |||
}); | |||
} | |||
#endif | |||
}; | |||
@@ -270,8 +290,6 @@ class WebBrowserComponent::Pimpl | |||
public: | |||
Pimpl (WebBrowserComponent* owner) | |||
{ | |||
ignoreUnused (owner); | |||
#if JUCE_MAC | |||
static WebViewKeyEquivalentResponder webviewClass; | |||
webView = (WKWebView*) webviewClass.createInstance(); | |||
@@ -421,20 +439,37 @@ private: | |||
static void runOpenPanel (id, SEL, WebView*, id<WebOpenPanelResultListener> resultListener, BOOL allowMultipleFiles) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser chooser (TRANS("Select the file you want to upload..."), | |||
File::getSpecialLocation (File::userHomeDirectory), "*"); | |||
struct DeletedFileChooserWrapper : private DeletedAtShutdown | |||
{ | |||
DeletedFileChooserWrapper (std::unique_ptr<FileChooser> fc, id<WebOpenPanelResultListener> rl) | |||
: chooser (std::move (fc)), listener (rl) | |||
{ | |||
[listener retain]; | |||
} | |||
~DeletedFileChooserWrapper() | |||
{ | |||
[listener release]; | |||
} | |||
std::unique_ptr<FileChooser> chooser; | |||
id<WebOpenPanelResultListener> listener; | |||
}; | |||
auto chooser = std::make_unique<FileChooser> (TRANS("Select the file you want to upload..."), | |||
File::getSpecialLocation (File::userHomeDirectory), "*"); | |||
auto* wrapper = new DeletedFileChooserWrapper (std::move (chooser), resultListener); | |||
auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | |||
| (allowMultipleFiles ? FileBrowserComponent::canSelectMultipleItems : 0); | |||
if (allowMultipleFiles ? chooser.browseForMultipleFilesToOpen() | |||
: chooser.browseForFileToOpen()) | |||
wrapper->chooser->launchAsync (flags, [wrapper] (const FileChooser&) | |||
{ | |||
for (auto& f : chooser.getResults()) | |||
[resultListener chooseFilename: juceStringToNS (f.getFullPathName())]; | |||
} | |||
#else | |||
ignoreUnused (resultListener, allowMultipleFiles); | |||
jassertfalse; // Can't use this without modal loops being enabled! | |||
#endif | |||
for (auto& f : wrapper->chooser->getResults()) | |||
[wrapper->listener chooseFilename: juceStringToNS (f.getFullPathName())]; | |||
delete wrapper; | |||
}); | |||
} | |||
}; | |||