See BREAKING-CHANGES.txt for more details.v6.1.6
| @@ -4,6 +4,31 @@ JUCE breaking changes | |||||
| Develop | 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 | Change | ||||
| ------ | ------ | ||||
| The minimum supported C++ standard is now C++14 and the oldest supported | The minimum supported C++ standard is now C++14 and the oldest supported | ||||
| @@ -590,14 +590,13 @@ private: | |||||
| if (fileChooser != nullptr) | if (fileChooser != nullptr) | ||||
| return; | return; | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | ||||
| { | { | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| RuntimePermissions::request (RuntimePermissions::readExternalStorage, | RuntimePermissions::request (RuntimePermissions::readExternalStorage, | ||||
| [safeThis] (bool granted) mutable | [safeThis] (bool granted) mutable | ||||
| { | { | ||||
| if (granted) | |||||
| if (safeThis != nullptr && granted) | |||||
| safeThis->openFile(); | safeThis->openFile(); | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -606,22 +605,19 @@ private: | |||||
| fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | ||||
| fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | 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) | if (fc.getURLResults().size() > 0) | ||||
| { | { | ||||
| auto u = fc.getURLResult(); | 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); | NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | ||||
| else | else | ||||
| safeThis->thumbnailComp.setCurrentURL (u); | |||||
| thumbnailComp.setCurrentURL (u); | |||||
| } | } | ||||
| safeThis->fileChooser = nullptr; | |||||
| fileChooser = nullptr; | |||||
| }, nullptr); | }, nullptr); | ||||
| } | } | ||||
| @@ -498,14 +498,13 @@ private: | |||||
| { | { | ||||
| if (btn == &chooseFileButton && fileChooser.get() == nullptr) | if (btn == &chooseFileButton && fileChooser.get() == nullptr) | ||||
| { | { | ||||
| SafePointer<AudioPlaybackDemo> safeThis (this); | |||||
| if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | ||||
| { | { | ||||
| SafePointer<AudioPlaybackDemo> safeThis (this); | |||||
| RuntimePermissions::request (RuntimePermissions::readExternalStorage, | RuntimePermissions::request (RuntimePermissions::readExternalStorage, | ||||
| [safeThis] (bool granted) mutable | [safeThis] (bool granted) mutable | ||||
| { | { | ||||
| if (granted) | |||||
| if (safeThis != nullptr && granted) | |||||
| safeThis->buttonClicked (&safeThis->chooseFileButton); | safeThis->buttonClicked (&safeThis->chooseFileButton); | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -516,16 +515,16 @@ private: | |||||
| fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | ||||
| fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | 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(); | auto u = fc.getURLResult(); | ||||
| safeThis->showAudioResource (std::move (u)); | |||||
| showAudioResource (std::move (u)); | |||||
| } | } | ||||
| safeThis->fileChooser = nullptr; | |||||
| fileChooser = nullptr; | |||||
| }, nullptr); | }, nullptr); | ||||
| } | } | ||||
| else | else | ||||
| @@ -590,14 +590,13 @@ private: | |||||
| if (fileChooser != nullptr) | if (fileChooser != nullptr) | ||||
| return; | return; | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | ||||
| { | { | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| RuntimePermissions::request (RuntimePermissions::readExternalStorage, | RuntimePermissions::request (RuntimePermissions::readExternalStorage, | ||||
| [safeThis] (bool granted) mutable | [safeThis] (bool granted) mutable | ||||
| { | { | ||||
| if (granted) | |||||
| if (safeThis != nullptr && granted) | |||||
| safeThis->openFile(); | safeThis->openFile(); | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -606,22 +605,19 @@ private: | |||||
| fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | ||||
| fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | 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) | if (fc.getURLResults().size() > 0) | ||||
| { | { | ||||
| auto u = fc.getURLResult(); | 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); | NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | ||||
| else | else | ||||
| safeThis->thumbnailComp.setCurrentURL (u); | |||||
| thumbnailComp.setCurrentURL (u); | |||||
| } | } | ||||
| safeThis->fileChooser = nullptr; | |||||
| fileChooser = nullptr; | |||||
| }, nullptr); | }, nullptr); | ||||
| } | } | ||||
| @@ -141,10 +141,21 @@ public: | |||||
| nativeButton.setButtonText ("Use Native Windows"); | nativeButton.setButtonText ("Use Native Windows"); | ||||
| nativeButton.onClick = [this] { getLookAndFeel().setUsingNativeAlertWindows (nativeButton.getToggleState()); }; | 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 | // warn in case we add any windows | ||||
| jassert (windowNames.size() == numDialogs); | jassert (windowNames.size() == numDialogs); | ||||
| @@ -207,11 +218,42 @@ private: | |||||
| OwnedArray<TextButton> windowButtons; | OwnedArray<TextButton> windowButtons; | ||||
| ToggleButton nativeButton; | 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) | void showWindow (Component& button, DialogType type) | ||||
| { | { | ||||
| @@ -232,7 +274,7 @@ private: | |||||
| AlertWindow::showOkCancelBox (AlertWindow::QuestionIcon, "This is an ok/cancel AlertWindow", | 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.", | "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) | else if (type == calloutBoxWindow) | ||||
| { | { | ||||
| @@ -247,31 +289,16 @@ private: | |||||
| } | } | ||||
| else if (type == extraComponentsAlertWindow) | 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) | else if (type == progressWindow) | ||||
| { | { | ||||
| @@ -461,6 +488,7 @@ private: | |||||
| ImagePreviewComponent imagePreview; | ImagePreviewComponent imagePreview; | ||||
| std::unique_ptr<FileChooser> fc; | std::unique_ptr<FileChooser> fc; | ||||
| std::unique_ptr<AlertWindow> asyncAlertWindow; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DialogsDemo) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DialogsDemo) | ||||
| }; | }; | ||||
| @@ -110,14 +110,12 @@ public: | |||||
| // not interested in this for now | // not interested in this for now | ||||
| } | } | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| File getSuggestedSaveAsFile (const File&) override | File getSuggestedSaveAsFile (const File&) override | ||||
| { | { | ||||
| return File::getSpecialLocation (File::userDesktopDirectory) | return File::getSpecialLocation (File::userDesktopDirectory) | ||||
| .getChildFile (getName()) | .getChildFile (getName()) | ||||
| .withFileExtension ("jnote"); | .withFileExtension ("jnote"); | ||||
| } | } | ||||
| #endif | |||||
| private: | private: | ||||
| Value textValueObject; | Value textValueObject; | ||||
| @@ -138,23 +136,19 @@ private: | |||||
| class DemoMultiDocumentPanel : public MultiDocumentPanel | class DemoMultiDocumentPanel : public MultiDocumentPanel | ||||
| { | { | ||||
| public: | 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)) | 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: | private: | ||||
| @@ -180,6 +174,16 @@ public: | |||||
| addNoteButton.onClick = [this] { addNote ("Note " + String (multiDocumentPanel.getNumDocuments() + 1), "Hello World!"); }; | addNoteButton.onClick = [this] { addNote ("Note " + String (multiDocumentPanel.getNumDocuments() + 1), "Hello World!"); }; | ||||
| addAndMakeVisible (addNoteButton); | addAndMakeVisible (addNoteButton); | ||||
| closeApplicationButton.onClick = [this] | |||||
| { | |||||
| multiDocumentPanel.closeAllDocumentsAsync (true, [] (bool allSaved) | |||||
| { | |||||
| if (allSaved) | |||||
| JUCEApplicationBase::quit(); | |||||
| }); | |||||
| }; | |||||
| addAndMakeVisible (closeApplicationButton); | |||||
| addAndMakeVisible (multiDocumentPanel); | addAndMakeVisible (multiDocumentPanel); | ||||
| multiDocumentPanel.setBackgroundColour (Colours::transparentBlack); | multiDocumentPanel.setBackgroundColour (Colours::transparentBlack); | ||||
| @@ -200,8 +204,9 @@ public: | |||||
| auto area = getLocalBounds(); | auto area = getLocalBounds(); | ||||
| auto buttonArea = area.removeFromTop (28).reduced (2); | 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); | multiDocumentPanel.setBounds (area); | ||||
| } | } | ||||
| @@ -235,11 +240,6 @@ public: | |||||
| } | } | ||||
| private: | private: | ||||
| ToggleButton showInTabsButton { "Show with tabs" }; | |||||
| TextButton addNoteButton { "Create a new note" }; | |||||
| DemoMultiDocumentPanel multiDocumentPanel; | |||||
| void updateLayoutMode() | void updateLayoutMode() | ||||
| { | { | ||||
| multiDocumentPanel.setLayoutMode (showInTabsButton.getToggleState() ? MultiDocumentPanel::MaximisedWindowsWithTabs | multiDocumentPanel.setLayoutMode (showInTabsButton.getToggleState() ? MultiDocumentPanel::MaximisedWindowsWithTabs | ||||
| @@ -261,5 +261,11 @@ private: | |||||
| createNotesForFiles (files); | 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) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MDIDemo) | ||||
| }; | }; | ||||
| @@ -1103,25 +1103,25 @@ private: | |||||
| void selectTexture (int itemID) | void selectTexture (int itemID) | ||||
| { | { | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| if (itemID == 1000) | 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())); | textures.add (new OpenGLUtils::TextureFromFile (fc.getResult())); | ||||
| updateTexturesList(); | updateTexturesList(); | ||||
| textureBox.setSelectedId (textures.size()); | textureBox.setSelectedId (textures.size()); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| else | else | ||||
| #endif | |||||
| { | { | ||||
| if (auto* t = textures[itemID - 1]) | if (auto* t = textures[itemID - 1]) | ||||
| demo.setTexture (t); | demo.setTexture (t); | ||||
| @@ -1135,10 +1135,8 @@ private: | |||||
| for (int i = 0; i < textures.size(); ++i) | for (int i = 0; i < textures.size(); ++i) | ||||
| textureBox.addItem (textures.getUnchecked (i)->name, i + 1); | textureBox.addItem (textures.getUnchecked (i)->name, i + 1); | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| textureBox.addSeparator(); | textureBox.addSeparator(); | ||||
| textureBox.addItem ("Load from a file...", 1000); | textureBox.addItem ("Load from a file...", 1000); | ||||
| #endif | |||||
| } | } | ||||
| void updateShader() | void updateShader() | ||||
| @@ -1208,6 +1206,8 @@ private: | |||||
| OwnedArray<OpenGLUtils::DemoTexture> textures; | OwnedArray<OpenGLUtils::DemoTexture> textures; | ||||
| std::unique_ptr<FileChooser> textureFileChooser; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoControlsOverlay) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoControlsOverlay) | ||||
| }; | }; | ||||
| @@ -590,14 +590,13 @@ private: | |||||
| if (fileChooser != nullptr) | if (fileChooser != nullptr) | ||||
| return; | return; | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage)) | ||||
| { | { | ||||
| SafePointer<AudioPlayerHeader> safeThis (this); | |||||
| RuntimePermissions::request (RuntimePermissions::readExternalStorage, | RuntimePermissions::request (RuntimePermissions::readExternalStorage, | ||||
| [safeThis] (bool granted) mutable | [safeThis] (bool granted) mutable | ||||
| { | { | ||||
| if (granted) | |||||
| if (safeThis != nullptr && granted) | |||||
| safeThis->openFile(); | safeThis->openFile(); | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -606,22 +605,19 @@ private: | |||||
| fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif")); | ||||
| fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, | 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) | if (fc.getURLResults().size() > 0) | ||||
| { | { | ||||
| auto u = fc.getURLResult(); | 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); | NativeMessageBox::showOkCancelBox (AlertWindow::WarningIcon, "Error loading file", "Unable to load audio file", nullptr, nullptr); | ||||
| else | else | ||||
| safeThis->thumbnailComp.setCurrentURL (u); | |||||
| thumbnailComp.setCurrentURL (u); | |||||
| } | } | ||||
| safeThis->fileChooser = nullptr; | |||||
| fileChooser = nullptr; | |||||
| }, 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 | // to flush any GUI events that may have been in transit before the app forces them to | ||||
| // be unloaded | // be unloaded | ||||
| new AsyncQuitRetrier(); | new AsyncQuitRetrier(); | ||||
| return; | |||||
| } | } | ||||
| else if (ModalComponentManager::getInstance()->cancelAllModalComponents()) | |||||
| if (ModalComponentManager::getInstance()->cancelAllModalComponents()) | |||||
| { | { | ||||
| new AsyncQuitRetrier(); | 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) | void MainHostWindow::changeListenerCallback (ChangeBroadcaster* changed) | ||||
| @@ -329,9 +351,20 @@ void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/ | |||||
| ->getValue ("recentFilterGraphFiles")); | ->getValue ("recentFilterGraphFiles")); | ||||
| if (graphHolder != nullptr) | if (graphHolder != nullptr) | ||||
| { | |||||
| if (auto* graph = graphHolder->graph.get()) | 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 | #endif | ||||
| else if (menuItemID >= 200 && menuItemID < 210) | else if (menuItemID >= 200 && menuItemID < 210) | ||||
| @@ -492,23 +525,43 @@ bool MainHostWindow::perform (const InvocationInfo& info) | |||||
| { | { | ||||
| #if ! (JUCE_IOS || JUCE_ANDROID) | #if ! (JUCE_IOS || JUCE_ANDROID) | ||||
| case CommandIDs::newFile: | 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; | break; | ||||
| case CommandIDs::open: | 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; | break; | ||||
| case CommandIDs::save: | case CommandIDs::save: | ||||
| if (graphHolder != nullptr && graphHolder->graph != nullptr) | if (graphHolder != nullptr && graphHolder->graph != nullptr) | ||||
| graphHolder->graph->save (true, true); | |||||
| graphHolder->graph->saveAsync (true, true, nullptr); | |||||
| break; | break; | ||||
| case CommandIDs::saveAs: | case CommandIDs::saveAs: | ||||
| if (graphHolder != nullptr && graphHolder->graph != nullptr) | if (graphHolder != nullptr && graphHolder->graph != nullptr) | ||||
| graphHolder->graph->saveAs (File(), true, true, true); | |||||
| graphHolder->graph->saveAsAsync ({}, true, true, true, nullptr); | |||||
| break; | break; | ||||
| #endif | #endif | ||||
| @@ -630,11 +683,22 @@ void MainHostWindow::filesDropped (const StringArray& files, int x, int y) | |||||
| if (graphHolder != nullptr) | if (graphHolder != nullptr) | ||||
| { | { | ||||
| #if ! (JUCE_ANDROID || JUCE_IOS) | #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 (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 | else | ||||
| #endif | #endif | ||||
| @@ -99,25 +99,34 @@ public: | |||||
| { | { | ||||
| createProjectButton.onClick = [this] | 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(); | 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; | getAppSettings().lastWizardFolder = dir; | ||||
| } | |||||
| } | |||||
| }); | |||||
| }); | |||||
| }; | }; | ||||
| addAndMakeVisible (createProjectButton); | addAndMakeVisible (createProjectButton); | ||||
| @@ -150,6 +159,7 @@ public: | |||||
| private: | private: | ||||
| NewProjectTemplates::ProjectTemplate projectTemplate; | NewProjectTemplates::ProjectTemplate projectTemplate; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| std::function<void (std::unique_ptr<Project>)> projectCreatedCallback; | std::function<void (std::unique_ptr<Project>)> projectCreatedCallback; | ||||
| ItemHeader header; | ItemHeader header; | ||||
| @@ -220,63 +220,91 @@ File NewProjectWizard::getLastWizardFolder() | |||||
| return lastFolderFallback; | 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; | StringArray failedFiles; | ||||
| if (! targetFolder.exists()) | if (! targetFolder.exists()) | ||||
| { | { | ||||
| if (! targetFolder.createDirectory()) | if (! targetFolder.createDirectory()) | ||||
| failedFiles.add (targetFolder.getFullPathName()); | |||||
| { | |||||
| displayFailedFilesMessage ({ targetFolder.getFullPathName() }); | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| else if (FileHelpers::containsAnyNonHiddenFiles (targetFolder)) | 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); | doBasicProjectSetup (*project, projectTemplate, name); | ||||
| StringArray failedFiles; | |||||
| if (addFiles (*project, projectTemplate, name, fileOptions, failedFiles)) | if (addFiles (*project, projectTemplate, name, fileOptions, failedFiles)) | ||||
| { | { | ||||
| addExporters (*project, *exporters.getArray()); | addExporters (*project, *exporters.getArray()); | ||||
| addModules (*project, *modules.getArray(), modulePath, useGlobalModulePath); | 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(); | 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) | 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())); | File file (fc.getResult().withFileExtension (AppearanceSettings::getSchemeFileSuffix())); | ||||
| getAppSettings().appearance.writeToFile (file); | getAppSettings().appearance.writeToFile (file); | ||||
| getAppSettings().appearance.refreshPresetSchemeList(); | getAppSettings().appearance.refreshPresetSchemeList(); | ||||
| saveSchemeState(); | saveSchemeState(); | ||||
| ProjucerApplication::getApp().selectEditorColourSchemeWithName (file.getFileNameWithoutExtension()); | ProjucerApplication::getApp().selectEditorColourSchemeWithName (file.getFileNameWithoutExtension()); | ||||
| } | |||||
| else if (isExit) | |||||
| { | |||||
| restorePreviousScheme(); | |||||
| } | |||||
| }); | |||||
| } | } | ||||
| void loadScheme() | 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())) | if (getAppSettings().appearance.readFromFile (fc.getResult())) | ||||
| { | { | ||||
| rebuildProperties(); | rebuildProperties(); | ||||
| saveSchemeState(); | saveSchemeState(); | ||||
| } | } | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| void lookAndFeelChanged() override | void lookAndFeelChanged() override | ||||
| @@ -264,6 +276,7 @@ private: | |||||
| appearance.getColourValue (colourNames[i]).setValue (colourValues[i]); | appearance.getColourValue (colourNames[i]).setValue (colourValues[i]); | ||||
| } | } | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| JUCE_DECLARE_NON_COPYABLE (EditorPanel) | JUCE_DECLARE_NON_COPYABLE (EditorPanel) | ||||
| }; | }; | ||||
| @@ -73,13 +73,20 @@ public: | |||||
| addAndMakeVisible (createButton); | addAndMakeVisible (createButton); | ||||
| createButton.onClick = [this] | 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); | pipTree.addListener (this); | ||||
| @@ -333,6 +340,8 @@ private: | |||||
| TextButton createButton { "Create PIP" }; | TextButton createButton { "Create PIP" }; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PIPCreatorWindowComponent) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PIPCreatorWindowComponent) | ||||
| }; | }; | ||||
| @@ -33,9 +33,9 @@ class TranslationToolComponent : public Component | |||||
| public: | public: | ||||
| TranslationToolComponent() | TranslationToolComponent() | ||||
| : editorOriginal (documentOriginal, nullptr), | : editorOriginal (documentOriginal, nullptr), | ||||
| editorPre (documentPre, nullptr), | |||||
| editorPost (documentPost, nullptr), | |||||
| editorResult (documentResult, nullptr) | |||||
| editorPre (documentPre, nullptr), | |||||
| editorPost (documentPost, nullptr), | |||||
| editorResult (documentResult, nullptr) | |||||
| { | { | ||||
| instructionsLabel.setText ( | instructionsLabel.setText ( | ||||
| "This utility converts translation files to/from a format that can be passed to automatic translation tools." | "This utility converts translation files to/from a format that can be passed to automatic translation tools." | ||||
| @@ -114,17 +114,7 @@ public: | |||||
| } | } | ||||
| private: | 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() | void generate() | ||||
| { | { | ||||
| StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent())); | StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent())); | ||||
| @@ -154,28 +144,35 @@ private: | |||||
| void scanFolder() | 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; | StringArray strings; | ||||
| TranslationHelpers::scanFolderForTranslations (strings, fc.getResult()); | TranslationHelpers::scanFolderForTranslations (strings, fc.getResult()); | ||||
| setPreTranslationText (TranslationHelpers::mungeStrings(strings)); | setPreTranslationText (TranslationHelpers::mungeStrings(strings)); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| void loadFile() | 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); | const LocalisedStrings loadedStrings (fc.getResult(), false); | ||||
| documentOriginal.replaceAllContent (fc.getResult().loadFileAsString().trim()); | documentOriginal.replaceAllContent (fc.getResult().loadFileAsString().trim()); | ||||
| setPreTranslationText (TranslationHelpers::getPreTranslationText (loadedStrings)); | setPreTranslationText (TranslationHelpers::getPreTranslationText (loadedStrings)); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| void setPreTranslationText (const String& text) | void setPreTranslationText (const String& text) | ||||
| @@ -184,4 +181,18 @@ private: | |||||
| editorPre.grabKeyboardFocus(); | editorPre.grabKeyboardFocus(); | ||||
| editorPre.selectAll(); | 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 | else | ||||
| { | { | ||||
| if (closeAllMainWindows()) | |||||
| quit(); | |||||
| closeAllMainWindows ([] (bool closedSuccessfully) | |||||
| { | |||||
| if (closedSuccessfully) | |||||
| ProjucerApplication::quit(); | |||||
| }); | |||||
| } | } | ||||
| } | } | ||||
| @@ -251,7 +254,7 @@ void ProjucerApplication::anotherInstanceStarted (const String& commandLine) | |||||
| ArgumentList list ({}, commandLine); | ArgumentList list ({}, commandLine); | ||||
| for (auto& arg : list.arguments) | 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? | // example doesn't exist? | ||||
| jassert (example != File()); | jassert (example != File()); | ||||
| openFile (example); | |||||
| openFile (example, nullptr); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -863,7 +866,7 @@ void ProjucerApplication::handleMainMenuCommand (int menuItemID) | |||||
| if (menuItemID >= recentProjectsBaseID && menuItemID < (recentProjectsBaseID + 100)) | if (menuItemID >= recentProjectsBaseID && menuItemID < (recentProjectsBaseID + 100)) | ||||
| { | { | ||||
| // open a file from the "recent files" menu | // 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)) | else if (menuItemID >= openWindowsBaseID && menuItemID < (openWindowsBaseID + 100)) | ||||
| { | { | ||||
| @@ -1095,23 +1098,33 @@ void ProjucerApplication::createNewProjectFromClipboard() | |||||
| tempFile.create(); | tempFile.create(); | ||||
| tempFile.appendText (SystemClipboard::getTextFromClipboard()); | 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)) | 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() | void ProjucerApplication::createNewPIP() | ||||
| @@ -1121,45 +1134,57 @@ void ProjucerApplication::createNewPIP() | |||||
| void ProjucerApplication::askUserToOpenFile() | 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() | void ProjucerApplication::saveAllDocuments() | ||||
| { | { | ||||
| openDocumentManager.saveAll(); | |||||
| openDocumentManager.saveAllSyncWithoutAsking(); | |||||
| for (int i = 0; i < mainWindowList.windows.size(); ++i) | for (int i = 0; i < mainWindowList.windows.size(); ++i) | ||||
| if (auto* pcc = mainWindowList.windows.getUnchecked(i)->getProjectContentComponent()) | if (auto* pcc = mainWindowList.windows.getUnchecked(i)->getProjectContentComponent()) | ||||
| pcc->refreshProjectTreeFileStatuses(); | 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() | 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 | #endif | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| void ProjucerApplication::clearRecentFiles() | void ProjucerApplication::clearRecentFiles() | ||||
| @@ -66,7 +66,7 @@ public: | |||||
| bool isGUIEditorEnabled() const; | bool isGUIEditorEnabled() const; | ||||
| //============================================================================== | //============================================================================== | ||||
| bool openFile (const File&); | |||||
| void openFile (const File&, std::function<void (bool)>); | |||||
| void showPathsWindow (bool highlightJUCEPath = false); | void showPathsWindow (bool highlightJUCEPath = false); | ||||
| PropertiesFile::Options getPropertyFileOptionsFor (const String& filename, bool isProjectSettings); | PropertiesFile::Options getPropertyFileOptionsFor (const String& filename, bool isProjectSettings); | ||||
| void selectEditorColourSchemeWithName (const String& schemeName); | void selectEditorColourSchemeWithName (const String& schemeName); | ||||
| @@ -119,8 +119,8 @@ private: | |||||
| void createNewPIP(); | void createNewPIP(); | ||||
| void askUserToOpenFile(); | void askUserToOpenFile(); | ||||
| void saveAllDocuments(); | void saveAllDocuments(); | ||||
| bool closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave); | |||||
| bool closeAllMainWindows(); | |||||
| void closeAllDocuments (OpenDocumentManager::SaveIfNeeded askUserToSave); | |||||
| void closeAllMainWindows (std::function<void (bool)>); | |||||
| void closeAllMainWindowsAndQuitIfNeeded(); | void closeAllMainWindowsAndQuitIfNeeded(); | ||||
| void clearRecentFiles(); | void clearRecentFiles(); | ||||
| @@ -216,6 +216,9 @@ private: | |||||
| int selectedColourSchemeIndex = 0, selectedEditorColourSchemeIndex = 0; | int selectedColourSchemeIndex = 0, selectedEditorColourSchemeIndex = 0; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjucerApplication) | 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) | 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 | // By default we will install into 'targetFolder/JUCE', but we should install into | ||||
| // 'targetFolder' if that is an existing JUCE directory. | // 'targetFolder' if that is an existing JUCE directory. | ||||
| @@ -259,6 +264,15 @@ void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const Version | |||||
| auto targetFolderPath = targetFolder.getFullPathName(); | 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 (willOverwriteJuceFolder) | ||||
| { | { | ||||
| if (targetFolder.getChildFile (".git").isDirectory()) | if (targetFolder.getChildFile (".git").isDirectory()) | ||||
| @@ -269,25 +283,32 @@ void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const Version | |||||
| return; | 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); | downloadAndInstall (asset, targetFolder); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString, | void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString, | ||||
| @@ -56,4 +56,7 @@ private: | |||||
| std::unique_ptr<DownloadAndInstallThread> installer; | std::unique_ptr<DownloadAndInstallThread> installer; | ||||
| std::unique_ptr<Component> dialogWindow; | std::unique_ptr<Component> dialogWindow; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (LatestVersionCheckerAndUpdater) | |||||
| }; | }; | ||||
| @@ -100,13 +100,18 @@ namespace | |||||
| if (fixMissingDependencies) | if (fixMissingDependencies) | ||||
| tryToFixMissingModuleDependencies(); | 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); | 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) | if (currentProject == nullptr) | ||||
| return true; | |||||
| { | |||||
| if (callback != nullptr) | |||||
| callback (true); | |||||
| return; | |||||
| } | |||||
| currentProject->getStoredProperties().setValue (getProjectWindowPosName(), getWindowStateAsString()); | currentProject->getStoredProperties().setValue (getProjectWindowPosName(), getWindowStateAsString()); | ||||
| @@ -242,27 +247,65 @@ bool MainWindow::closeCurrentProject (OpenDocumentManager::SaveIfNeeded askUserT | |||||
| pcc->hideEditor(); | 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) | 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) | void MainWindow::setProject (std::unique_ptr<Project> newProject) | ||||
| @@ -308,44 +351,102 @@ bool MainWindow::canOpenFile (const File& file) const | |||||
| || ProjucerApplication::getApp().openDocumentManager.canOpenFile (file)); | || 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)) | if (file.hasFileExtension (Project::projectFileExtension)) | ||||
| { | { | ||||
| auto newDoc = std::make_unique<Project> (file); | auto newDoc = std::make_unique<Project> (file); | ||||
| auto result = newDoc->loadFrom (file, true); | 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()) | if (generatorResult != Result::ok()) | ||||
| { | { | ||||
| @@ -353,29 +454,47 @@ bool MainWindow::openPIP (PIPGenerator generator) | |||||
| "PIP Error.", | "PIP Error.", | ||||
| generatorResult.getErrorMessage()); | generatorResult.getErrorMessage()); | ||||
| return false; | |||||
| if (callback != nullptr) | |||||
| callback (false); | |||||
| return; | |||||
| } | } | ||||
| if (! generator.createMainCpp()) | |||||
| if (! generator->createMainCpp()) | |||||
| { | { | ||||
| AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | ||||
| "PIP Error.", | "PIP Error.", | ||||
| "Failed to create Main.cpp."); | "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) | void MainWindow::setupTemporaryPIPProject (PIPGenerator& generator) | ||||
| @@ -408,15 +527,32 @@ bool MainWindow::isInterestedInFileDrag (const StringArray& filenames) | |||||
| return false; | 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, | bool MainWindow::shouldDropFilesWhenDraggedExternally (const DragAndDropTarget::SourceDetails& sourceDetails, | ||||
| @@ -472,7 +608,7 @@ void MainWindow::showStartPage() | |||||
| jassert (currentProject == nullptr); | jassert (currentProject == nullptr); | ||||
| setContentOwned (new StartPageComponent ([this] (std::unique_ptr<Project>&& newProject) { setProject (std::move (newProject)); }, | 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); | true); | ||||
| setResizable (false, false); | setResizable (false, false); | ||||
| @@ -580,19 +716,38 @@ void MainWindowList::forceCloseAllWindows() | |||||
| windows.clear(); | 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() | void MainWindowList::createWindowIfNoneAreOpen() | ||||
| @@ -613,11 +768,18 @@ void MainWindowList::closeWindow (MainWindow* w) | |||||
| else | else | ||||
| #endif | #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); | 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()) | if (! file.exists()) | ||||
| return false; | |||||
| { | |||||
| if (callback != nullptr) | |||||
| callback (false); | |||||
| return; | |||||
| } | |||||
| for (auto* w : windows) | for (auto* w : windows) | ||||
| { | { | ||||
| if (w->getProject() != nullptr && w->getProject()->getFile() == file) | if (w->getProject() != nullptr && w->getProject()->getFile() == file) | ||||
| { | { | ||||
| w->toFront (true); | w->toFront (true); | ||||
| return true; | |||||
| if (callback != nullptr) | |||||
| callback (true); | |||||
| return; | |||||
| } | } | ||||
| } | } | ||||
| WeakReference<MainWindowList> parent { this }; | |||||
| if (file.hasFileExtension (Project::projectFileExtension) | if (file.hasFileExtension (Project::projectFileExtension) | ||||
| || isPIPFile (file)) | || isPIPFile (file)) | ||||
| { | { | ||||
| @@ -675,23 +848,37 @@ bool MainWindowList::openFile (const File& file, bool openInBackground) | |||||
| auto* w = getOrCreateEmptyWindow(); | auto* w = getOrCreateEmptyWindow(); | ||||
| jassert (w != nullptr); | 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() | MainWindow* MainWindowList::createNewMainWindow() | ||||
| @@ -841,7 +1028,7 @@ void MainWindowList::reopenLastProjects() | |||||
| for (auto& p : getAppSettings().getLastProjects()) | for (auto& p : getAppSettings().getLastProjects()) | ||||
| if (p.existsAsFile()) | if (p.existsAsFile()) | ||||
| openFile (p, true); | |||||
| openFile (p, nullptr, true); | |||||
| } | } | ||||
| void MainWindowList::sendLookAndFeelChange() | void MainWindowList::sendLookAndFeelChange() | ||||
| @@ -53,7 +53,7 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| bool canOpenFile (const File& file) const; | 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); | void setProject (std::unique_ptr<Project> newProject); | ||||
| Project* getProject() const { return currentProject.get(); } | Project* getProject() const { return currentProject.get(); } | ||||
| @@ -61,7 +61,7 @@ public: | |||||
| void makeVisible(); | void makeVisible(); | ||||
| void restoreWindowPosition(); | void restoreWindowPosition(); | ||||
| void updateTitleBarIcon(); | void updateTitleBarIcon(); | ||||
| bool closeCurrentProject (OpenDocumentManager::SaveIfNeeded askToSave); | |||||
| void closeCurrentProject (OpenDocumentManager::SaveIfNeeded askToSave, std::function<void (bool)> callback); | |||||
| void moveProject (File newProjectFile, OpenInIDE openInIDE); | void moveProject (File newProjectFile, OpenInIDE openInIDE); | ||||
| void showStartPage(); | void showStartPage(); | ||||
| @@ -91,7 +91,7 @@ private: | |||||
| static const char* getProjectWindowPosName() { return "projectWindowPos"; } | static const char* getProjectWindowPosName() { return "projectWindowPos"; } | ||||
| void createProjectContentCompIfNeeded(); | void createProjectContentCompIfNeeded(); | ||||
| bool openPIP (PIPGenerator); | |||||
| void openPIP (const File&, std::function<void (bool)> callback); | |||||
| void setupTemporaryPIPProject (PIPGenerator&); | void setupTemporaryPIPProject (PIPGenerator&); | ||||
| void initialiseProjectWindow(); | void initialiseProjectWindow(); | ||||
| @@ -112,14 +112,14 @@ public: | |||||
| MainWindowList(); | MainWindowList(); | ||||
| void forceCloseAllWindows(); | void forceCloseAllWindows(); | ||||
| bool askAllWindowsToClose(); | |||||
| void askAllWindowsToClose (std::function<void (bool)> callback); | |||||
| void closeWindow (MainWindow*); | void closeWindow (MainWindow*); | ||||
| void goToSiblingWindow (MainWindow*, int delta); | void goToSiblingWindow (MainWindow*, int delta); | ||||
| void createWindowIfNoneAreOpen(); | void createWindowIfNoneAreOpen(); | ||||
| void openDocument (OpenDocumentManager::Document*, bool grabFocus); | 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* createNewMainWindow(); | ||||
| MainWindow* getFrontmostWindow (bool createIfNotFound = true); | MainWindow* getFrontmostWindow (bool createIfNotFound = true); | ||||
| @@ -142,4 +142,5 @@ private: | |||||
| bool isInReopenLastProjects = false; | bool isInReopenLastProjects = false; | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindowList) | 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; } | bool refersToProject (Project& p) const override { return project == &p; } | ||||
| Project* getProject() const override { return project; } | Project* getProject() const override { return project; } | ||||
| bool needsSaving() const override { return false; } | 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(); } | bool hasFileBeenModifiedExternally() override { return fileModificationTime != file.getLastModificationTime(); } | ||||
| void reloadFromFile() override { fileModificationTime = file.getLastModificationTime(); } | void reloadFromFile() override { fileModificationTime = file.getLastModificationTime(); } | ||||
| String getName() const override { return file.getFileName(); } | String getName() const override { return file.getFileName(); } | ||||
| @@ -164,86 +165,201 @@ OpenDocumentManager::Document* OpenDocumentManager::getOpenDocument (int index) | |||||
| return documents.getUnchecked (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()) | 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; | bool canClose = true; | ||||
| for (int i = listeners.size(); --i >= 0;) | for (int i = listeners.size(); --i >= 0;) | ||||
| if (DocumentCloseListener* l = listeners[i]) | |||||
| if (auto* l = listeners[i]) | |||||
| if (! l->documentAboutToClose (doc)) | if (! l->documentAboutToClose (doc)) | ||||
| canClose = false; | canClose = false; | ||||
| if (! canClose) | if (! canClose) | ||||
| return false; | return false; | ||||
| documents.remove (index); | |||||
| documents.removeObject (doc); | |||||
| ProjucerApplication::getCommandManager().commandStatusChanged(); | ProjucerApplication::getCommandManager().commandStatusChanged(); | ||||
| } | } | ||||
| return true; | 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;) | for (int i = documents.size(); --i >= 0;) | ||||
| if (Document* d = documents[i]) | |||||
| if (auto* d = documents[i]) | |||||
| if (d->isForFile (f)) | 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;) | for (int i = documents.size(); --i >= 0;) | ||||
| if (Document* d = documents[i]) | if (Document* d = documents[i]) | ||||
| if (d->refersToProject (project)) | if (d->refersToProject (project)) | ||||
| if (! closeDocument (i, saveIfNeeded)) | |||||
| return false; | |||||
| return true; | |||||
| closeDocumentWithoutSaving (d); | |||||
| } | } | ||||
| bool OpenDocumentManager::anyFilesNeedSaving() const | bool OpenDocumentManager::anyFilesNeedSaving() const | ||||
| @@ -255,17 +371,13 @@ bool OpenDocumentManager::anyFilesNeedSaving() const | |||||
| return false; | return false; | ||||
| } | } | ||||
| bool OpenDocumentManager::saveAll() | |||||
| void OpenDocumentManager::saveAllSyncWithoutAsking() | |||||
| { | { | ||||
| for (int i = documents.size(); --i >= 0;) | 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() | void OpenDocumentManager::reloadModifiedFiles() | ||||
| @@ -51,8 +51,9 @@ public: | |||||
| virtual String getType() const = 0; | virtual String getType() const = 0; | ||||
| virtual File getFile() const = 0; | virtual File getFile() const = 0; | ||||
| virtual bool needsSaving() 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 bool hasFileBeenModifiedExternally() = 0; | ||||
| virtual void reloadFromFile() = 0; | virtual void reloadFromFile() = 0; | ||||
| virtual std::unique_ptr<Component> createEditor() = 0; | virtual std::unique_ptr<Component> createEditor() = 0; | ||||
| @@ -72,14 +73,20 @@ public: | |||||
| bool canOpenFile (const File& file); | bool canOpenFile (const File& file); | ||||
| Document* openFile (Project* project, 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 anyFilesNeedSaving() const; | ||||
| bool saveAll(); | |||||
| FileBasedDocument::SaveResult saveIfNeededAndUserAgrees (Document* doc); | |||||
| void saveAllSyncWithoutAsking(); | |||||
| void saveIfNeededAndUserAgrees (Document* doc, std::function<void (FileBasedDocument::SaveResult)>); | |||||
| void reloadModifiedFiles(); | void reloadModifiedFiles(); | ||||
| void fileHasBeenRenamed (const File& oldFile, const File& newFile); | void fileHasBeenRenamed (const File& oldFile, const File& newFile); | ||||
| @@ -112,11 +119,19 @@ public: | |||||
| private: | private: | ||||
| //============================================================================== | |||||
| void closeLastDocumentUsingProjectRecursive (WeakReference<OpenDocumentManager>, | |||||
| Project*, | |||||
| SaveIfNeeded, | |||||
| std::function<void (bool)>); | |||||
| //============================================================================== | |||||
| OwnedArray<DocumentType> types; | OwnedArray<DocumentType> types; | ||||
| OwnedArray<Document> documents; | OwnedArray<Document> documents; | ||||
| Array<DocumentCloseListener*> listeners; | Array<DocumentCloseListener*> listeners; | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenDocumentManager) | 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(); | return temp.overwriteTargetFileWithTemporary(); | ||||
| } | } | ||||
| bool SourceCodeDocument::save() | |||||
| bool SourceCodeDocument::saveSyncWithoutAsking() | |||||
| { | { | ||||
| if (writeCodeDocToFile (getFile(), getCodeDocument())) | if (writeCodeDocToFile (getFile(), getCodeDocument())) | ||||
| { | { | ||||
| @@ -110,14 +110,28 @@ bool SourceCodeDocument::save() | |||||
| return false; | 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) | void SourceCodeDocument::updateLastState (CodeEditorComponent& editor) | ||||
| @@ -642,18 +656,31 @@ void CppCodeEditorComponent::performPopupMenuAction (int menuItemID) | |||||
| void CppCodeEditorComponent::insertComponentClass() | 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(); | auto className = aw.getTextEditorContents (classNameField).trim(); | ||||
| if (className == build_tools::makeValidIdentifier (className, false, true, false)) | if (className == build_tools::makeValidIdentifier (className, false, true, false)) | ||||
| @@ -661,8 +688,10 @@ void CppCodeEditorComponent::insertComponentClass() | |||||
| String code (BinaryData::jucer_InlineComponentTemplate_h); | String code (BinaryData::jucer_InlineComponentTemplate_h); | ||||
| code = code.replace ("%%component_class%%", className); | code = code.replace ("%%component_class%%", className); | ||||
| insertTextAtCaret (code); | |||||
| break; | |||||
| parent->insertTextAtCaret (code); | |||||
| return; | |||||
| } | } | ||||
| } | |||||
| parent->insertComponentClass(); | |||||
| })); | |||||
| } | } | ||||
| @@ -80,8 +80,9 @@ public: | |||||
| } | } | ||||
| void reloadFromFile() override; | 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> createEditor() override; | ||||
| std::unique_ptr<Component> createViewer() override { return createEditor(); } | std::unique_ptr<Component> createViewer() override { return createEditor(); } | ||||
| @@ -132,6 +133,9 @@ protected: | |||||
| std::unique_ptr<CodeEditorComponent::State> lastState; | std::unique_ptr<CodeEditorComponent::State> lastState; | ||||
| void reloadInternal(); | void reloadInternal(); | ||||
| private: | |||||
| std::unique_ptr<FileChooser> chooser; | |||||
| }; | }; | ||||
| class GenericCodeEditorComponent; | class GenericCodeEditorComponent; | ||||
| @@ -235,5 +239,7 @@ public: | |||||
| private: | private: | ||||
| void insertComponentClass(); | void insertComponentClass(); | ||||
| std::unique_ptr<AlertWindow> asyncAlertWindow; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CppCodeEditorComponent) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CppCodeEditorComponent) | ||||
| }; | }; | ||||
| @@ -140,7 +140,7 @@ protected: | |||||
| String colourIdCode, colourName, xmlTagName; | String colourIdCode, colourName, xmlTagName; | ||||
| }; | }; | ||||
| OwnedArray <ComponentColourInfo> colours; | |||||
| OwnedArray<ComponentColourInfo> colours; | |||||
| private: | private: | ||||
| JUCE_DECLARE_NON_COPYABLE (ComponentTypeHandler) | JUCE_DECLARE_NON_COPYABLE (ComponentTypeHandler) | ||||
| @@ -672,13 +672,13 @@ private: | |||||
| m.addItem (i + 1, "Delete tab " + String (i) | m.addItem (i + 1, "Delete tab " + String (i) | ||||
| + ": \"" + names[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 | String getButtonText() const | ||||
| @@ -1131,11 +1131,13 @@ private: | |||||
| m.addItem (1, "Move this tab up", tabIndex > 0); | m.addItem (1, "Move this tab up", tabIndex > 0); | ||||
| m.addItem (2, "Move this tab down", tabIndex < totalNumTabs - 1); | 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 | String getButtonText() const | ||||
| @@ -74,14 +74,16 @@ public: | |||||
| { | { | ||||
| if (newIndex == 0) | 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 | else | ||||
| { | { | ||||
| @@ -660,7 +660,6 @@ void PaintElement::updateSiblingComps() | |||||
| } | } | ||||
| } | } | ||||
| void PaintElement::showPopupMenu() | void PaintElement::showPopupMenu() | ||||
| { | { | ||||
| auto* commandManager = &ProjucerApplication::getCommandManager(); | auto* commandManager = &ProjucerApplication::getCommandManager(); | ||||
| @@ -685,5 +684,5 @@ void PaintElement::showPopupMenu() | |||||
| m.addCommandItem (commandManager, StandardApplicationCommandIDs::paste); | m.addCommandItem (commandManager, StandardApplicationCommandIDs::paste); | ||||
| m.addCommandItem (commandManager, StandardApplicationCommandIDs::del); | m.addCommandItem (commandManager, StandardApplicationCommandIDs::del); | ||||
| m.show(); | |||||
| m.showMenuAsync ({}); | |||||
| } | } | ||||
| @@ -124,7 +124,7 @@ protected: | |||||
| void siblingComponentsChanged(); | void siblingComponentsChanged(); | ||||
| OwnedArray <ElementSiblingComponent> siblingComponents; | |||||
| OwnedArray<ElementSiblingComponent> siblingComponents; | |||||
| void updateSiblingComps(); | void updateSiblingComps(); | ||||
| @@ -127,7 +127,7 @@ public: | |||||
| private: | private: | ||||
| friend class PathPoint; | friend class PathPoint; | ||||
| friend class PathPointComponent; | friend class PathPointComponent; | ||||
| OwnedArray <PathPoint> points; | |||||
| OwnedArray<PathPoint> points; | |||||
| bool nonZeroWinding; | bool nonZeroWinding; | ||||
| mutable Path path; | mutable Path path; | ||||
| mutable Rectangle<int> lastPathBounds; | mutable Rectangle<int> lastPathBounds; | ||||
| @@ -63,8 +63,15 @@ public: | |||||
| button.setConnectedEdges (TextButton::ConnectedOnLeft | TextButton::ConnectedOnRight); | button.setConnectedEdges (TextButton::ConnectedOnLeft | TextButton::ConnectedOnRight); | ||||
| button.onClick = [this] | 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)); | textEditor.reset (new PositionPropLabel (*this)); | ||||
| @@ -173,7 +180,7 @@ public: | |||||
| refresh(); | refresh(); | ||||
| } | } | ||||
| bool showMenu (ComponentLayout* compLayout) | |||||
| void showMenu (ComponentLayout* compLayout, std::function<void (bool)> callback) | |||||
| { | { | ||||
| RelativePositionedRectangle rpr (getPosition()); | RelativePositionedRectangle rpr (getPosition()); | ||||
| PositionedRectangle p (rpr.rect); | PositionedRectangle p (rpr.rect); | ||||
| @@ -255,127 +262,135 @@ public: | |||||
| m.addSubMenu ("Relative to", compLayout->getRelativeTargetMenu (component, (int) dimension)); | 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() | void resized() | ||||
| @@ -282,7 +282,7 @@ void ComponentLayoutEditor::mouseDown (const MouseEvent& e) | |||||
| for (int i = 0; i < ObjectTypes::numComponentTypes; ++i) | for (int i = 0; i < ObjectTypes::numComponentTypes; ++i) | ||||
| m.addCommandItem (commandManager, JucerCommandIDs::newComponentBase + i); | m.addCommandItem (commandManager, JucerCommandIDs::newComponentBase + i); | ||||
| m.show(); | |||||
| m.showMenuAsync (PopupMenu::Options()); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -387,7 +387,7 @@ bool ComponentLayoutEditor::isInterestedInDragSource (const SourceDetails& dragS | |||||
| void ComponentLayoutEditor::itemDropped (const SourceDetails& dragSourceDetails) | void ComponentLayoutEditor::itemDropped (const SourceDetails& dragSourceDetails) | ||||
| { | { | ||||
| OwnedArray <Project::Item> selectedNodes; | |||||
| OwnedArray<Project::Item> selectedNodes; | |||||
| ProjectContentComponent::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); | ProjectContentComponent::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); | ||||
| StringArray filenames; | StringArray filenames; | ||||
| @@ -212,7 +212,7 @@ void PaintRoutineEditor::mouseDown (const MouseEvent& e) | |||||
| for (int i = 0; i < ObjectTypes::numElementTypes; ++i) | for (int i = 0; i < ObjectTypes::numElementTypes; ++i) | ||||
| m.addCommandItem (commandManager, JucerCommandIDs::newElementBase + i); | m.addCommandItem (commandManager, JucerCommandIDs::newElementBase + i); | ||||
| m.show(); | |||||
| m.showMenuAsync (PopupMenu::Options()); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -39,7 +39,7 @@ public: | |||||
| { | { | ||||
| if (auto* r = document.getResources() [row]) | if (auto* r = document.getResources() [row]) | ||||
| document.getResources().browseForResource ("Select a file to replace this resource", "*", | 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") | delButton ("Delete selected resources") | ||||
| { | { | ||||
| addAndMakeVisible (addButton); | 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); | addAndMakeVisible (reloadAllButton); | ||||
| reloadAllButton.onClick = [this] { reloadAll(); }; | reloadAllButton.onClick = [this] { reloadAll(); }; | ||||
| @@ -258,16 +261,12 @@ void ResourceEditorPanel::reloadAll() | |||||
| StringArray failed; | StringArray failed; | ||||
| for (int i = 0; i < document.getResources().size(); ++i) | for (int i = 0; i < document.getResources().size(); ++i) | ||||
| { | |||||
| if (! document.getResources().reload (i)) | if (! document.getResources().reload (i)) | ||||
| failed.add (document.getResources().getResourceNames() [i]); | failed.add (document.getResources().getResourceNames() [i]); | ||||
| } | |||||
| if (failed.size() > 0) | 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" | #include "jucer_JucerDocument.h" | ||||
| //============================================================================== | //============================================================================== | ||||
| BinaryResources::BinaryResources() | |||||
| { | |||||
| } | |||||
| BinaryResources::~BinaryResources() | |||||
| { | |||||
| } | |||||
| BinaryResources& BinaryResources::operator= (const BinaryResources& other) | BinaryResources& BinaryResources::operator= (const BinaryResources& other) | ||||
| { | { | ||||
| for (auto* r : other.resources) | for (auto* r : other.resources) | ||||
| @@ -130,15 +122,20 @@ bool BinaryResources::reload (const int index) | |||||
| File (resources [index]->originalFilename)); | 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); | String name (resourceToReplace); | ||||
| if (name.isEmpty()) | if (name.isEmpty()) | ||||
| @@ -146,17 +143,15 @@ String BinaryResources::browseForResource (const String& title, | |||||
| if (! add (name, fc.getResult())) | 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(); | name.clear(); | ||||
| } | } | ||||
| return name; | |||||
| } | |||||
| return {}; | |||||
| callback (name); | |||||
| }); | |||||
| } | } | ||||
| String BinaryResources::findUniqueName (const String& rootName) const | String BinaryResources::findUniqueName (const String& rootName) const | ||||
| @@ -36,9 +36,6 @@ class BinaryResources | |||||
| { | { | ||||
| public: | public: | ||||
| //============================================================================== | //============================================================================== | ||||
| BinaryResources(); | |||||
| ~BinaryResources(); | |||||
| BinaryResources& operator= (const BinaryResources& other); | BinaryResources& operator= (const BinaryResources& other); | ||||
| void loadFromCpp (const File& cppFileLocation, const String& cpp); | 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 add (const String& name, const String& originalFileName, const MemoryBlock& data); | ||||
| void remove (const int index); | void remove (const int index); | ||||
| bool reload (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; | String findUniqueName (const String& rootName) const; | ||||
| @@ -86,12 +84,13 @@ public: | |||||
| void fillInGeneratedCode (GeneratedCode& code) const; | void fillInGeneratedCode (GeneratedCode& code) const; | ||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| JucerDocument* document; | |||||
| OwnedArray <BinaryResource> resources; | |||||
| BinaryResource* findResource (const String& name) const noexcept; | BinaryResource* findResource (const String& name) const noexcept; | ||||
| void changed(); | void changed(); | ||||
| //============================================================================== | |||||
| JucerDocument* document; | |||||
| OwnedArray<BinaryResource> resources; | |||||
| std::unique_ptr<FileChooser> chooser; | |||||
| }; | }; | ||||
| @@ -122,7 +122,7 @@ public: | |||||
| private: | private: | ||||
| JucerDocument* document; | JucerDocument* document; | ||||
| OwnedArray <Component> components; | |||||
| OwnedArray<Component> components; | |||||
| SelectedItemSet <Component*> selected; | SelectedItemSet <Component*> selected; | ||||
| int nextCompUID; | 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; | auto& odm = ProjucerApplication::getApp().openDocumentManager; | ||||
| if (auto* header = odm.openFile (nullptr, getFile().withFileExtension (".h"))) | 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 | std::unique_ptr<Component> createEditor() override | ||||
| @@ -733,6 +760,8 @@ public: | |||||
| bool canOpenFile (const File& f) override { return JucerDocument::isValidJucerCppFile (f); } | bool canOpenFile (const File& f) override { return JucerDocument::isValidJucerCppFile (f); } | ||||
| Document* openFile (Project* p, const File& f) override { return new JucerComponentDocument (p, f); } | Document* openFile (Project* p, const File& f) override { return new JucerComponentDocument (p, f); } | ||||
| }; | }; | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (JucerComponentDocument) | |||||
| }; | }; | ||||
| OpenDocumentManager::DocumentType* createGUIDocumentType(); | OpenDocumentManager::DocumentType* createGUIDocumentType(); | ||||
| @@ -744,53 +773,65 @@ OpenDocumentManager::DocumentType* createGUIDocumentType() | |||||
| //============================================================================== | //============================================================================== | ||||
| struct NewGUIComponentWizard : public NewFileWizard::Type | struct NewGUIComponentWizard : public NewFileWizard::Type | ||||
| { | { | ||||
| NewGUIComponentWizard() {} | |||||
| NewGUIComponentWizard (Project& proj) | |||||
| : project (proj) | |||||
| {} | |||||
| String getName() override { return "GUI Component"; } | 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: | private: | ||||
| OwnedArray <PaintElement> elements; | |||||
| OwnedArray<PaintElement> elements; | |||||
| SelectedItemSet <PaintElement*> selectedElements; | SelectedItemSet <PaintElement*> selectedElements; | ||||
| SelectedItemSet <PathPoint*> selectedPoints; | SelectedItemSet <PathPoint*> selectedPoints; | ||||
| JucerDocument* document; | JucerDocument* document; | ||||
| @@ -664,15 +664,16 @@ void EnabledModulesList::addModuleInteractive (const String& moduleID) | |||||
| void EnabledModulesList::addModuleFromUserSelectedFile() | 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) | void EnabledModulesList::addModuleOfferingToCopy (const File& f, bool isFromUserSpecifiedFolder) | ||||
| @@ -142,5 +142,7 @@ private: | |||||
| CriticalSection stateLock; | CriticalSection stateLock; | ||||
| ValueTree state; | ValueTree state; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EnabledModulesList) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EnabledModulesList) | ||||
| }; | }; | ||||
| @@ -78,13 +78,25 @@ public: | |||||
| void deleteItem() override | 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 | void addSubItems() override | ||||
| @@ -117,7 +129,7 @@ public: | |||||
| if (resultCode == 1) | if (resultCode == 1) | ||||
| exporter->addNewConfiguration (false); | exporter->addNewConfiguration (false); | ||||
| else if (resultCode == 2) | else if (resultCode == 2) | ||||
| project.saveProject (exporter.get()); | |||||
| project.saveProject (Async::yes, exporter.get(), nullptr); | |||||
| else if (resultCode == 3) | else if (resultCode == 3) | ||||
| deleteAllSelectedItems(); | deleteAllSelectedItems(); | ||||
| } | } | ||||
| @@ -200,6 +212,7 @@ private: | |||||
| }; | }; | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ExporterItem) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ExporterItem) | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (ExporterItem) | |||||
| }; | }; | ||||
| @@ -231,12 +244,24 @@ public: | |||||
| void deleteItem() override | 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 | void showPopupMenu (Point<int> p) override | ||||
| @@ -297,6 +322,8 @@ private: | |||||
| }; | }; | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConfigItem) | 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; | 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()) | if (! f.moveToTrash()) | ||||
| { | { | ||||
| @@ -136,15 +122,48 @@ public: | |||||
| pcc->hideEditor(); | pcc->hideEditor(); | ||||
| } | } | ||||
| om.closeFile (itemToRemove->getFile(), OpenDocumentManager::SaveIfNeeded::no); | |||||
| om.closeFileWithoutSaving (itemToRemove->getFile()); | |||||
| itemToRemove->deleteItem(); | 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 | virtual void revealInFinder() const | ||||
| @@ -155,17 +174,24 @@ public: | |||||
| virtual void browseToAddExistingFiles() | virtual void browseToAddExistingFiles() | ||||
| { | { | ||||
| auto location = item.isGroup() ? item.determineGroupFolder() : getFile(); | 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; | StringArray files; | ||||
| for (int i = 0; i < fc.getResults().size(); ++i) | for (int i = 0; i < fc.getResults().size(); ++i) | ||||
| files.add (fc.getResults().getReference(i).getFullPathName()); | files.add (fc.getResults().getReference(i).getFullPathName()); | ||||
| addFilesRetainingSortOrder (files); | addFilesRetainingSortOrder (files); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| virtual void checkFileStatus() // (recursive) | virtual void checkFileStatus() // (recursive) | ||||
| @@ -192,7 +218,7 @@ public: | |||||
| p->addFilesRetainingSortOrder (files); | p->addFilesRetainingSortOrder (files); | ||||
| } | } | ||||
| virtual void moveSelectedItemsTo (OwnedArray <Project::Item>&, int /*insertIndex*/) | |||||
| virtual void moveSelectedItemsTo (OwnedArray<Project::Item>&, int /*insertIndex*/) | |||||
| { | { | ||||
| jassertfalse; | jassertfalse; | ||||
| } | } | ||||
| @@ -269,7 +295,7 @@ public: | |||||
| void filesDropped (const StringArray& files, int insertIndex) override | void filesDropped (const StringArray& files, int insertIndex) override | ||||
| { | { | ||||
| if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension)) | if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension)) | ||||
| ProjucerApplication::getApp().openFile (files[0]); | |||||
| ProjucerApplication::getApp().openFile (files[0], [] (bool) {}); | |||||
| else | else | ||||
| addFilesAtIndex (files, insertIndex); | addFilesAtIndex (files, insertIndex); | ||||
| } | } | ||||
| @@ -444,6 +470,11 @@ protected: | |||||
| return -1; | 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 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 | String getDisplayName() const override | ||||
| { | { | ||||
| @@ -490,8 +521,9 @@ public: | |||||
| { | { | ||||
| if (newName != File::createLegalFileName (newName)) | 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); | triggerAsyncRename (item); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -506,30 +538,42 @@ public: | |||||
| if (correspondingItem.isValid()) | 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; | return; | ||||
| } | } | ||||
| if (! correspondingItem.renameFile (newFile.withFileExtension (correspondingFile.getFileExtension()))) | 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)) | 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; | break; | ||||
| } | } | ||||
| } | } | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (SourceFileItem) | |||||
| }; | }; | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -799,7 +845,7 @@ public: | |||||
| m.addItem (1002, "Add Existing Files..."); | m.addItem (1002, "Add Existing Files..."); | ||||
| m.addSeparator(); | m.addSeparator(); | ||||
| NewFileWizard().addWizardsToMenu (m); | |||||
| wizard.addWizardsToMenu (m); | |||||
| } | } | ||||
| void processCreateFileMenuItem (int menuID) | void processCreateFileMenuItem (int menuID) | ||||
| @@ -811,7 +857,7 @@ public: | |||||
| default: | default: | ||||
| jassert (getProject() != nullptr); | jassert (getProject() != nullptr); | ||||
| NewFileWizard().runWizardFromMenu (menuID, *getProject(), item); | |||||
| wizard.runWizardFromMenu (menuID, *getProject(), item); | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -832,4 +878,5 @@ public: | |||||
| } | } | ||||
| String searchFilter; | String searchFilter; | ||||
| NewFileWizard wizard; | |||||
| }; | }; | ||||
| @@ -270,7 +270,7 @@ private: | |||||
| Array<Value> exporterModulePathValues, globalPathValues; | Array<Value> exporterModulePathValues, globalPathValues; | ||||
| Value useGlobalPathValue; | Value useGlobalPathValue; | ||||
| OwnedArray <Project::ConfigFlag> configFlags; | |||||
| OwnedArray<Project::ConfigFlag> configFlags; | |||||
| PropertyGroupComponent group; | PropertyGroupComponent group; | ||||
| Project& project; | Project& project; | ||||
| @@ -248,7 +248,7 @@ void HeaderComponent::initialiseButtons() | |||||
| else | else | ||||
| { | { | ||||
| if (auto exporter = getSelectedExporter()) | if (auto exporter = getSelectedExporter()) | ||||
| project->openProjectInIDE (*exporter, true); | |||||
| project->openProjectInIDE (*exporter, true, nullptr); | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -28,7 +28,12 @@ | |||||
| #include "Sidebar/jucer_Sidebar.h" | #include "Sidebar/jucer_Sidebar.h" | ||||
| NewFileWizard::Type* createGUIComponentWizard(); | |||||
| struct WizardHolder | |||||
| { | |||||
| std::unique_ptr<NewFileWizard::Type> wizard; | |||||
| }; | |||||
| NewFileWizard::Type* createGUIComponentWizard (Project&); | |||||
| //============================================================================== | //============================================================================== | ||||
| ProjectContentComponent::ProjectContentComponent() | ProjectContentComponent::ProjectContentComponent() | ||||
| @@ -305,7 +310,7 @@ void ProjectContentComponent::closeDocument() | |||||
| if (currentDocument != nullptr) | if (currentDocument != nullptr) | ||||
| { | { | ||||
| ProjucerApplication::getApp().openDocumentManager | ProjucerApplication::getApp().openDocumentManager | ||||
| .closeDocument (currentDocument, OpenDocumentManager::SaveIfNeeded::yes); | |||||
| .closeDocumentAsync (currentDocument, OpenDocumentManager::SaveIfNeeded::yes, nullptr); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -315,35 +320,49 @@ void ProjectContentComponent::closeDocument() | |||||
| static void showSaveWarning (OpenDocumentManager::Document* currentDocument) | 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 != 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 | else | ||||
| { | { | ||||
| saveProject(); | |||||
| saveProjectAsync(); | |||||
| } | } | ||||
| } | } | ||||
| void ProjectContentComponent::saveAs() | |||||
| void ProjectContentComponent::saveAsAsync() | |||||
| { | { | ||||
| if (currentDocument != nullptr) | 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; | return false; | ||||
| } | } | ||||
| bool ProjectContentComponent::saveProject() | |||||
| void ProjectContentComponent::saveProjectAsync() | |||||
| { | { | ||||
| if (project != nullptr) | if (project != nullptr) | ||||
| return (project->save (true, true) == FileBasedDocument::savedOk); | |||||
| return false; | |||||
| project->saveAsync (true, true, nullptr); | |||||
| } | } | ||||
| void ProjectContentComponent::closeProject() | void ProjectContentComponent::closeProject() | ||||
| { | { | ||||
| if (auto* mw = findParentComponentOfClass<MainWindow>()) | if (auto* mw = findParentComponentOfClass<MainWindow>()) | ||||
| mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes); | |||||
| mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, nullptr); | |||||
| } | } | ||||
| void ProjectContentComponent::showProjectSettings() | void ProjectContentComponent::showProjectSettings() | ||||
| @@ -481,7 +498,7 @@ void ProjectContentComponent::openInSelectedIDE (bool saveFirst) | |||||
| { | { | ||||
| if (project != nullptr) | if (project != nullptr) | ||||
| if (auto selectedExporter = headerComponent.getSelectedExporter()) | if (auto selectedExporter = headerComponent.getSelectedExporter()) | ||||
| project->openProjectInIDE (*selectedExporter, saveFirst); | |||||
| project->openProjectInIDE (*selectedExporter, saveFirst, nullptr); | |||||
| } | } | ||||
| void ProjectContentComponent::showNewExporterMenu() | void ProjectContentComponent::showNewExporterMenu() | ||||
| @@ -787,7 +804,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica | |||||
| bool ProjectContentComponent::perform (const InvocationInfo& info) | bool ProjectContentComponent::perform (const InvocationInfo& info) | ||||
| { | { | ||||
| // don't allow the project to be saved again if it's currently saving | // 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; | return false; | ||||
| switch (info.commandID) | switch (info.commandID) | ||||
| @@ -818,14 +835,14 @@ bool ProjectContentComponent::perform (const InvocationInfo& info) | |||||
| switch (info.commandID) | 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::showProjectSettings: showProjectSettings(); break; | ||||
| case CommandIDs::showFileExplorerPanel: showFilesPanel(); break; | case CommandIDs::showFileExplorerPanel: showFilesPanel(); break; | ||||
| @@ -866,8 +883,9 @@ void ProjectContentComponent::addNewGUIFile() | |||||
| { | { | ||||
| if (project != nullptr) | 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" | #include "jucer_ContentViewComponent.h" | ||||
| class Sidebar; | class Sidebar; | ||||
| struct WizardHolder; | |||||
| //============================================================================== | //============================================================================== | ||||
| class ProjectContentComponent : public Component, | class ProjectContentComponent : public Component, | ||||
| @@ -57,8 +58,8 @@ public: | |||||
| void hideDocument (OpenDocumentManager::Document*); | void hideDocument (OpenDocumentManager::Document*); | ||||
| OpenDocumentManager::Document* getCurrentDocument() const { return currentDocument; } | OpenDocumentManager::Document* getCurrentDocument() const { return currentDocument; } | ||||
| void closeDocument(); | void closeDocument(); | ||||
| void saveDocument(); | |||||
| void saveAs(); | |||||
| void saveDocumentAsync(); | |||||
| void saveAsAsync(); | |||||
| void hideEditor(); | void hideEditor(); | ||||
| void setScrollableEditorComponent (std::unique_ptr<Component> component); | void setScrollableEditorComponent (std::unique_ptr<Component> component); | ||||
| @@ -72,7 +73,7 @@ public: | |||||
| bool canGoToCounterpart() const; | bool canGoToCounterpart() const; | ||||
| bool goToCounterpart(); | bool goToCounterpart(); | ||||
| bool saveProject(); | |||||
| void saveProjectAsync(); | |||||
| void closeProject(); | void closeProject(); | ||||
| void openInSelectedIDE (bool saveFirst); | void openInSelectedIDE (bool saveFirst); | ||||
| void showNewExporterMenu(); | void showNewExporterMenu(); | ||||
| @@ -145,6 +146,8 @@ private: | |||||
| bool isForeground = false; | bool isForeground = false; | ||||
| int lastViewedTab = 0; | int lastViewedTab = 0; | ||||
| std::unique_ptr<WizardHolder> wizardHolder; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectContentComponent) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectContentComponent) | ||||
| }; | }; | ||||
| @@ -66,12 +66,22 @@ void Project::ProjectFileModificationPoller::reloadProjectFromDisk() | |||||
| { | { | ||||
| if (auto* mw = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (projectFile)) | 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() | void Project::ProjectFileModificationPoller::resaveProject() | ||||
| { | { | ||||
| reset(); | reset(); | ||||
| project.saveProject(); | |||||
| project.saveProject (Async::yes, nullptr, nullptr); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -122,7 +132,7 @@ Project::~Project() | |||||
| auto& app = ProjucerApplication::getApp(); | auto& app = ProjucerApplication::getApp(); | ||||
| app.openDocumentManager.closeAllDocumentsUsingProject (*this, OpenDocumentManager::SaveIfNeeded::no); | |||||
| app.openDocumentManager.closeAllDocumentsUsingProjectWithoutSaving (*this); | |||||
| if (! app.isRunningCommandLine) | if (! app.isRunningCommandLine) | ||||
| app.getLicenseController().removeListener (this); | app.getLicenseController().removeListener (this); | ||||
| @@ -397,9 +407,9 @@ void Project::removeDefunctExporters() | |||||
| if (ProjucerApplication::getApp().isRunningCommandLine) | if (ProjucerApplication::getApp().isRunningCommandLine) | ||||
| std::cout << "WARNING! The " + oldExporters[key] + " Exporter is deprecated. The exporter will be removed from this project." << std::endl; | std::cout << "WARNING! The " + oldExporters[key] + " Exporter is deprecated. The exporter will be removed from this project." << std::endl; | ||||
| else | 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); | exporters.removeChild (oldExporter, nullptr); | ||||
| } | } | ||||
| @@ -664,40 +674,73 @@ Result Project::saveDocument (const File& file) | |||||
| jassert (file == getFile()); | jassert (file == getFile()); | ||||
| ignoreUnused (file); | 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()) | 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()) | if (isTemporaryProject()) | ||||
| { | { | ||||
| saveAndMoveTemporaryProject (false); | saveAndMoveTemporaryProject (false); | ||||
| return Result::ok(); | |||||
| onCompletion (Result::ok()); | |||||
| return; | |||||
| } | } | ||||
| updateProjectSettings(); | updateProjectSettings(); | ||||
| if (! ProjucerApplication::getApp().isRunningCommandLine) | if (! ProjucerApplication::getApp().isRunningCommandLine) | ||||
| { | { | ||||
| ProjucerApplication::getApp().openDocumentManager.saveAll(); | |||||
| ProjucerApplication::getApp().openDocumentManager.saveAllSyncWithoutAsking(); | |||||
| if (! isTemporaryProject()) | if (! isTemporaryProject()) | ||||
| registerRecentFile (getFile()); | 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();) | for (ExporterIterator exporter (*this); exporter.next();) | ||||
| { | { | ||||
| @@ -706,33 +749,57 @@ Result Project::openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirs | |||||
| if (isTemporaryProject()) | if (isTemporaryProject()) | ||||
| { | { | ||||
| saveAndMoveTemporaryProject (true); | saveAndMoveTemporaryProject (true); | ||||
| return Result::ok(); | |||||
| if (onCompletion != nullptr) | |||||
| onCompletion (Result::ok()); | |||||
| return; | |||||
| } | } | ||||
| if (saveFirst) | 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(); | exporter->launchProject(); | ||||
| break; | |||||
| } | } | ||||
| } | } | ||||
| return Result::ok(); | |||||
| if (onCompletion != nullptr) | |||||
| onCompletion (Result::ok()); | |||||
| } | } | ||||
| Result Project::saveResourcesOnly() | Result Project::saveResourcesOnly() | ||||
| { | { | ||||
| ProjectSaver saver (*this); | |||||
| return saver.saveResourcesOnly(); | |||||
| saver = std::make_unique<ProjectSaver> (*this); | |||||
| return saver->saveResourcesOnly(); | |||||
| } | } | ||||
| bool Project::hasIncompatibleLicenseTypeAndSplashScreenSetting() const | bool Project::hasIncompatibleLicenseTypeAndSplashScreenSetting() const | ||||
| @@ -988,37 +1055,41 @@ void Project::setTemporaryDirectory (const File& dir) noexcept | |||||
| void Project::saveAndMoveTemporaryProject (bool openInIDE) | 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 (Project& p) : index (-1), project (p) {} | ||||
| Project::ExporterIterator::~ExporterIterator() {} | |||||
| bool Project::ExporterIterator::next() | bool Project::ExporterIterator::next() | ||||
| { | { | ||||
| @@ -31,6 +31,7 @@ | |||||
| class ProjectExporter; | class ProjectExporter; | ||||
| class LibraryModule; | class LibraryModule; | ||||
| class EnabledModulesList; | class EnabledModulesList; | ||||
| class ProjectSaver; | |||||
| namespace ProjectMessages | namespace ProjectMessages | ||||
| { | { | ||||
| @@ -109,6 +110,8 @@ namespace ProjectMessages | |||||
| using MessageAction = std::pair<String, std::function<void()>>; | using MessageAction = std::pair<String, std::function<void()>>; | ||||
| } | } | ||||
| enum class Async { no, yes }; | |||||
| //============================================================================== | //============================================================================== | ||||
| class Project : public FileBasedDocument, | class Project : public FileBasedDocument, | ||||
| private ValueTree::Listener, | private ValueTree::Listener, | ||||
| @@ -125,10 +128,11 @@ public: | |||||
| String getDocumentTitle() override; | String getDocumentTitle() override; | ||||
| Result loadDocument (const File& file) override; | Result loadDocument (const File& file) override; | ||||
| Result saveDocument (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 saveResourcesOnly(); | ||||
| Result openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst); | |||||
| void openProjectInIDE (ProjectExporter& exporterToOpen, bool saveFirst, std::function<void (Result)> onCompletion); | |||||
| File getLastDocumentOpened() override; | File getLastDocumentOpened() override; | ||||
| void setLastDocumentOpened (const File& file) override; | void setLastDocumentOpened (const File& file) override; | ||||
| @@ -433,7 +437,6 @@ public: | |||||
| struct ExporterIterator | struct ExporterIterator | ||||
| { | { | ||||
| ExporterIterator (Project& project); | ExporterIterator (Project& project); | ||||
| ~ExporterIterator(); | |||||
| bool next(); | bool next(); | ||||
| @@ -486,7 +489,7 @@ public: | |||||
| String getUniqueTargetFolderSuffixForExporter (const Identifier& exporterIdentifier, const String& baseTargetFolder); | 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(); } | bool isTemporaryProject() const noexcept { return tempDirectory != File(); } | ||||
| File getTemporaryDirectory() const noexcept { return tempDirectory; } | File getTemporaryDirectory() const noexcept { return tempDirectory; } | ||||
| @@ -572,7 +575,6 @@ private: | |||||
| //============================================================================== | //============================================================================== | ||||
| friend class Item; | friend class Item; | ||||
| bool isSaving = false; | |||||
| StringPairArray parsedPreprocessorDefs; | StringPairArray parsedPreprocessorDefs; | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -612,12 +614,21 @@ private: | |||||
| void updateCLionWarning (bool showWarning); | void updateCLionWarning (bool showWarning); | ||||
| void updateModuleNotFoundWarning (bool showWarning); | void updateModuleNotFoundWarning (bool showWarning); | ||||
| void openProjectInIDEImpl (ExporterIterator exporter, | |||||
| String exporterToOpen, | |||||
| bool saveFirst, | |||||
| std::function<void (Result)> onCompletion); | |||||
| ValueTree projectMessages { ProjectMessages::Ids::projectMessages, {}, | ValueTree projectMessages { ProjectMessages::Ids::projectMessages, {}, | ||||
| { { ProjectMessages::Ids::notification, {} }, { ProjectMessages::Ids::warning, {} } } }; | { { ProjectMessages::Ids::notification, {} }, { ProjectMessages::Ids::warning, {} } } }; | ||||
| std::map<Identifier, std::vector<ProjectMessages::MessageAction>> messageActions; | std::map<Identifier, std::vector<ProjectMessages::MessageAction>> messageActions; | ||||
| ProjectFileModificationPoller fileModificationPoller { *this }; | ProjectFileModificationPoller fileModificationPoller { *this }; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| std::unique_ptr<ProjectSaver> saver; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Project) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Project) | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (Project) | |||||
| }; | }; | ||||
| @@ -42,17 +42,32 @@ ProjectSaver::ProjectSaver (Project& p) | |||||
| generatedFilesGroup.setID (generatedGroupID); | 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() | Result ProjectSaver::saveResourcesOnly() | ||||
| @@ -36,7 +36,7 @@ class ProjectSaver | |||||
| public: | public: | ||||
| ProjectSaver (Project& projectToSave); | ProjectSaver (Project& projectToSave); | ||||
| Result save (ProjectExporter* exporterToSave = nullptr); | |||||
| void save (Async async, ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion); | |||||
| Result saveResourcesOnly(); | Result saveResourcesOnly(); | ||||
| void saveBasicProjectItems (const OwnedArray<LibraryModule>& modules, const String& appConfigUserContent); | void saveBasicProjectItems (const OwnedArray<LibraryModule>& modules, const String& appConfigUserContent); | ||||
| @@ -52,21 +52,30 @@ private: | |||||
| struct SaveThreadWithProgressWindow : public ThreadWithProgressWindow | struct SaveThreadWithProgressWindow : public ThreadWithProgressWindow | ||||
| { | { | ||||
| public: | public: | ||||
| SaveThreadWithProgressWindow (ProjectSaver& ps, ProjectExporter* exporterToSave) | |||||
| SaveThreadWithProgressWindow (ProjectSaver& ps, | |||||
| ProjectExporter* exporterToSave, | |||||
| std::function<void (Result)> onCompletionIn) | |||||
| : ThreadWithProgressWindow ("Saving...", true, false), | : ThreadWithProgressWindow ("Saving...", true, false), | ||||
| saver (ps), | saver (ps), | ||||
| specifiedExporterToSave (exporterToSave) | |||||
| {} | |||||
| specifiedExporterToSave (exporterToSave), | |||||
| onCompletion (std::move (onCompletionIn)) | |||||
| { | |||||
| jassert (onCompletion != nullptr); | |||||
| } | |||||
| void run() override | void run() override | ||||
| { | { | ||||
| setProgress (-1); | 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; | ProjectSaver& saver; | ||||
| Result result = Result::ok(); | |||||
| ProjectExporter* specifiedExporterToSave; | ProjectExporter* specifiedExporterToSave; | ||||
| std::function<void (Result)> onCompletion; | |||||
| JUCE_DECLARE_NON_COPYABLE (SaveThreadWithProgressWindow) | JUCE_DECLARE_NON_COPYABLE (SaveThreadWithProgressWindow) | ||||
| }; | }; | ||||
| @@ -86,6 +95,7 @@ private: | |||||
| OwnedArray<LibraryModule> getModules(); | OwnedArray<LibraryModule> getModules(); | ||||
| Result saveProject (ProjectExporter* specifiedExporterToSave); | Result saveProject (ProjectExporter* specifiedExporterToSave); | ||||
| void saveProjectAsync (ProjectExporter* exporterToSave, std::function<void (Result)> onCompletion); | |||||
| template <typename WriterCallback> | template <typename WriterCallback> | ||||
| void writeOrRemoveGeneratedFile (const String& name, WriterCallback&& writerCallback); | void writeOrRemoveGeneratedFile (const String& name, WriterCallback&& writerCallback); | ||||
| @@ -118,8 +128,11 @@ private: | |||||
| CriticalSection errorLock; | CriticalSection errorLock; | ||||
| StringArray errors; | StringArray errors; | ||||
| std::unique_ptr<SaveThreadWithProgressWindow> saveThread; | |||||
| bool hasBinaryData = false; | bool hasBinaryData = false; | ||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectSaver) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectSaver) | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (ProjectSaver) | |||||
| }; | }; | ||||
| @@ -60,16 +60,15 @@ namespace | |||||
| class NewCppFileWizard : public NewFileWizard::Type | class NewCppFileWizard : public NewFileWizard::Type | ||||
| { | { | ||||
| public: | public: | ||||
| NewCppFileWizard() {} | |||||
| String getName() override { return "CPP File"; } | String getName() override { return "CPP File"; } | ||||
| void createNewFile (Project&, Project::Item parent) override | 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) | static bool create (Project::Item parent, const File& newFile, const char* templateName) | ||||
| @@ -89,16 +88,15 @@ public: | |||||
| class NewHeaderFileWizard : public NewFileWizard::Type | class NewHeaderFileWizard : public NewFileWizard::Type | ||||
| { | { | ||||
| public: | public: | ||||
| NewHeaderFileWizard() {} | |||||
| String getName() override { return "Header File"; } | String getName() override { return "Header File"; } | ||||
| void createNewFile (Project&, Project::Item parent) override | 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) | static bool create (Project::Item parent, const File& newFile, const char* templateName) | ||||
| @@ -118,19 +116,15 @@ public: | |||||
| class NewCppAndHeaderFileWizard : public NewFileWizard::Type | class NewCppAndHeaderFileWizard : public NewFileWizard::Type | ||||
| { | { | ||||
| public: | public: | ||||
| NewCppAndHeaderFileWizard() {} | |||||
| String getName() override { return "CPP & Header File"; } | String getName() override { return "CPP & Header File"; } | ||||
| void createNewFile (Project&, Project::Item parent) override | 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")) | if (NewCppFileWizard::create (parent, newFile.withFileExtension ("h"), "jucer_NewCppFileTemplate_h")) | ||||
| NewCppFileWizard::create (parent, newFile.withFileExtension ("cpp"), "jucer_NewCppFileTemplate_cpp"); | NewCppFileWizard::create (parent, newFile.withFileExtension ("cpp"), "jucer_NewCppFileTemplate_cpp"); | ||||
| } | |||||
| }); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -138,37 +132,11 @@ public: | |||||
| class NewComponentFileWizard : public NewFileWizard::Type | class NewComponentFileWizard : public NewFileWizard::Type | ||||
| { | { | ||||
| public: | public: | ||||
| NewComponentFileWizard() {} | |||||
| String getName() override { return "Component class (split between a CPP & header)"; } | String getName() override { return "Component class (split between a CPP & header)"; } | ||||
| void createNewFile (Project&, Project::Item parent) override | 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, | static bool create (const String& className, Project::Item parent, | ||||
| @@ -198,14 +166,61 @@ private: | |||||
| } | } | ||||
| static String getClassNameFieldName() { return "Class Name"; } | 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 | class NewSingleFileComponentFileWizard : public NewComponentFileWizard | ||||
| { | { | ||||
| public: | public: | ||||
| NewSingleFileComponentFileWizard() {} | |||||
| String getName() override { return "Component class (in a single source file)"; } | String getName() override { return "Component class (in a single source file)"; } | ||||
| void createFiles (Project::Item parent, const String& className, const File& newFile) override | void createFiles (Project::Item parent, const String& className, const File& newFile) override | ||||
| @@ -218,24 +233,28 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| void NewFileWizard::Type::showFailedToWriteMessage (const File& file) | 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: | 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); | static void showFailedToWriteMessage (const File& file); | ||||
| private: | |||||
| std::unique_ptr<FileChooser> chooser; | |||||
| }; | }; | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -134,17 +134,29 @@ private: | |||||
| if (isDirectory) | 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 | 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; | String wildcards; | ||||
| File root; | File root; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilePathPropertyComponent) | 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. */ | /** Pops up a dialog letting the user save the processor's state to a file. */ | ||||
| void askUserToSaveState (const String& fileSuffix = String()) | 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); | setLastFile (fc); | ||||
| MemoryBlock data; | MemoryBlock data; | ||||
| @@ -205,20 +212,23 @@ public: | |||||
| AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | ||||
| TRANS("Error whilst saving"), | TRANS("Error whilst saving"), | ||||
| TRANS("Couldn't write to the specified file!")); | 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. */ | /** Pops up a dialog letting the user re-load the processor's state from a file. */ | ||||
| void askUserToLoadState (const String& fileSuffix = String()) | 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); | setLastFile (fc); | ||||
| MemoryBlock data; | MemoryBlock data; | ||||
| @@ -229,10 +239,7 @@ public: | |||||
| AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | ||||
| TRANS("Error whilst loading"), | TRANS("Error whilst loading"), | ||||
| TRANS("Couldn't read from the specified file!")); | TRANS("Couldn't read from the specified file!")); | ||||
| } | |||||
| #else | |||||
| ignoreUnused (fileSuffix); | |||||
| #endif | |||||
| }); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -407,6 +414,8 @@ public: | |||||
| std::unique_ptr<AudioDeviceManager::AudioDeviceSetup> options; | std::unique_ptr<AudioDeviceManager::AudioDeviceSetup> options; | ||||
| Array<MidiDeviceInfo> lastMidiDevices; | Array<MidiDeviceInfo> lastMidiDevices; | ||||
| std::unique_ptr<FileChooser> stateFileChooser; | |||||
| private: | private: | ||||
| /* This class can be used to ensure that audio callbacks use buffers with a | /* This class can be used to ensure that audio callbacks use buffers with a | ||||
| predictable maximum size. | predictable maximum size. | ||||
| @@ -335,7 +335,7 @@ namespace juce | |||||
| #elif ! defined (JUCE_MODAL_LOOPS_PERMITTED) | #elif ! defined (JUCE_MODAL_LOOPS_PERMITTED) | ||||
| /** Some operating environments don't provide a modal loop mechanism, so this flag can be | /** 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. */ | 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 | #endif | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -79,7 +79,7 @@ public: | |||||
| */ | */ | ||||
| bool hasStopMessageBeenSent() const noexcept { return quitMessagePosted.get() != 0; } | 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. | /** Synchronously dispatches messages until a given time has elapsed. | ||||
| Returns false if a quit message has been posted by a call to stopDispatchLoop(), | Returns false if a quit message has been posted by a call to stopDispatchLoop(), | ||||
| @@ -2020,7 +2020,7 @@ public: | |||||
| virtual void handleCommandMessage (int commandId); | 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. | /** Runs a component modally, waiting until the loop terminates. | ||||
| This method first makes the component visible, brings it to the front and | This method first makes the component visible, brings it to the front and | ||||
| @@ -119,7 +119,7 @@ public: | |||||
| */ | */ | ||||
| bool cancelAllModalComponents(); | 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 | /** Runs the event loop until the currently topmost modal component is dismissed, and | ||||
| returns the exit code for that component. | returns the exit code for that component. | ||||
| */ | */ | ||||
| @@ -164,6 +164,7 @@ class JUCE_API ModalCallbackFunction | |||||
| public: | public: | ||||
| /** This is a utility function to create a ModalComponentManager::Callback that will | /** This is a utility function to create a ModalComponentManager::Callback that will | ||||
| call a lambda function. | call a lambda function. | ||||
| The lambda that you supply must take an integer parameter, which is the result code that | The lambda that you supply must take an integer parameter, which is the result code that | ||||
| was returned when the modal component was dismissed. | 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. | 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 | e.g. @code | ||||
| void loadMooseFile() | 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); | loadMoose (mooseFile); | ||||
| } | } | ||||
| @@ -125,7 +121,7 @@ public: | |||||
| ~FileChooser(); | ~FileChooser(); | ||||
| //============================================================================== | //============================================================================== | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| /** Shows a dialog box to choose a file to open. | /** Shows a dialog box to choose a file to open. | ||||
| This will display the dialog box modally, using an "open file" mode, so that | 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. | browseForFileToOpen() for more info about the behaviour of this method. | ||||
| */ | */ | ||||
| bool browseForMultipleFilesOrDirectories (FilePreviewComponent* previewComponent = nullptr); | bool browseForMultipleFilesOrDirectories (FilePreviewComponent* previewComponent = nullptr); | ||||
| #endif | |||||
| //============================================================================== | //============================================================================== | ||||
| /** Runs a dialog box for the given set of option flags. | /** Runs a dialog box for the given set of option flags. | ||||
| @@ -193,6 +188,7 @@ public: | |||||
| @see FileBrowserComponent::FileChooserFlags | @see FileBrowserComponent::FileChooserFlags | ||||
| */ | */ | ||||
| bool showDialog (int flags, FilePreviewComponent* previewComponent); | bool showDialog (int flags, FilePreviewComponent* previewComponent); | ||||
| #endif | |||||
| /** Use this method to launch the file browser window asynchronously. | /** Use this method to launch the file browser window asynchronously. | ||||
| @@ -200,12 +196,9 @@ public: | |||||
| structure and will launch it modally, returning immediately. | structure and will launch it modally, returning immediately. | ||||
| You must specify a callback which is called when the file browser is | 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. | 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 | You must ensure that the lifetime of the callback object is longer than | ||||
| the lifetime of the file-chooser. | 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 | This is a Juce-based file dialog box; to use a native file chooser, see the | ||||
| FileChooser class. | FileChooser class. | ||||
| To use one of these, create it and call its show() method. e.g. | |||||
| @code | @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 | @endcode | ||||
| @@ -99,7 +104,7 @@ public: | |||||
| ~FileChooserDialogBox() override; | ~FileChooserDialogBox() override; | ||||
| //============================================================================== | //============================================================================== | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| /** Displays and runs the dialog box modally. | /** Displays and runs the dialog box modally. | ||||
| This will show the box with the specified size, returning true if the user | 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) | 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.remove (row); | ||||
| path.add (chooser.getResult(), row); | |||||
| path.add (fc.getResult(), row); | |||||
| changed(); | changed(); | ||||
| } | |||||
| #else | |||||
| ignoreUnused (row); | |||||
| #endif | |||||
| }); | |||||
| } | } | ||||
| void FileSearchPathListComponent::listBoxItemDoubleClicked (int row, const MouseEvent&) | void FileSearchPathListComponent::listBoxItemDoubleClicked (int row, const MouseEvent&) | ||||
| @@ -226,16 +226,17 @@ void FileSearchPathListComponent::addPath() | |||||
| if (start == File()) | if (start == File()) | ||||
| start = File::getCurrentWorkingDirectory(); | 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() | void FileSearchPathListComponent::deleteSelected() | ||||
| @@ -100,6 +100,7 @@ private: | |||||
| //============================================================================== | //============================================================================== | ||||
| FileSearchPath path; | FileSearchPath path; | ||||
| File defaultBrowseTarget; | File defaultBrowseTarget; | ||||
| std::unique_ptr<FileChooser> chooser; | |||||
| ListBox listBox; | ListBox listBox; | ||||
| TextButton addButton, removeButton, changeButton; | TextButton addButton, removeButton, changeButton; | ||||
| @@ -114,22 +114,22 @@ File FilenameComponent::getLocationToBrowse() | |||||
| void FilenameComponent::showChooser() | 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&) | bool FilenameComponent::isInterestedInFileDrag (const StringArray&) | ||||
| @@ -73,21 +73,21 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| /** Creates a FilenameComponent. | /** 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 | @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, | FilenameComponent (const String& name, | ||||
| const File& currentFile, | const File& currentFile, | ||||
| @@ -216,6 +216,10 @@ public: | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| void handleAsyncUpdate() override; | |||||
| void showChooser(); | |||||
| ComboBox filenameBox; | ComboBox filenameBox; | ||||
| String lastFilename; | String lastFilename; | ||||
| std::unique_ptr<Button> browseButton; | std::unique_ptr<Button> browseButton; | ||||
| @@ -224,9 +228,7 @@ private: | |||||
| String wildcard, enforcedSuffix, browseButtonText; | String wildcard, enforcedSuffix, browseButtonText; | ||||
| ListenerList <FilenameComponentListener> listeners; | ListenerList <FilenameComponentListener> listeners; | ||||
| File defaultBrowseFile; | File defaultBrowseFile; | ||||
| void showChooser(); | |||||
| void handleAsyncUpdate() override; | |||||
| std::unique_ptr<FileChooser> chooser; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilenameComponent) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilenameComponent) | ||||
| }; | }; | ||||
| @@ -45,7 +45,6 @@ public: | |||||
| /** Destructor. */ | /** Destructor. */ | ||||
| ~ImagePreviewComponent() override; | ~ImagePreviewComponent() override; | ||||
| //============================================================================== | //============================================================================== | ||||
| /** @internal */ | /** @internal */ | ||||
| void selectedFileChanged (const File& newSelectedFile) override; | void selectedFileChanged (const File& newSelectedFile) override; | ||||
| @@ -48,7 +48,7 @@ void MultiDocumentPanelWindow::maximiseButtonPressed() | |||||
| void MultiDocumentPanelWindow::closeButtonPressed() | void MultiDocumentPanelWindow::closeButtonPressed() | ||||
| { | { | ||||
| if (auto* owner = getOwner()) | if (auto* owner = getOwner()) | ||||
| owner->closeDocument (getContentComponent(), true); | |||||
| owner->closeDocumentAsync (getContentComponent(), true, nullptr); | |||||
| else | else | ||||
| jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel! | jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel! | ||||
| } | } | ||||
| @@ -95,10 +95,7 @@ MultiDocumentPanel::MultiDocumentPanel() | |||||
| setOpaque (true); | setOpaque (true); | ||||
| } | } | ||||
| MultiDocumentPanel::~MultiDocumentPanel() | |||||
| { | |||||
| closeAllDocuments (false); | |||||
| } | |||||
| MultiDocumentPanel::~MultiDocumentPanel() = default; | |||||
| //============================================================================== | //============================================================================== | ||||
| namespace MultiDocHelpers | namespace MultiDocHelpers | ||||
| @@ -109,6 +106,7 @@ namespace MultiDocHelpers | |||||
| } | } | ||||
| } | } | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst) | bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst) | ||||
| { | { | ||||
| while (! components.isEmpty()) | while (! components.isEmpty()) | ||||
| @@ -117,6 +115,52 @@ bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst) | |||||
| return true; | 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() | MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow() | ||||
| { | { | ||||
| @@ -217,93 +261,105 @@ bool MultiDocumentPanel::addDocument (Component* const component, | |||||
| return true; | return true; | ||||
| } | } | ||||
| bool MultiDocumentPanel::closeDocument (Component* component, | |||||
| const bool checkItsOkToCloseFirst) | |||||
| void MultiDocumentPanel::closeDocumentInternal (Component* component) | |||||
| { | { | ||||
| // Intellisense warns about component being uninitialised. | // Intellisense warns about component being uninitialised. | ||||
| // I'm not sure how a function argument could be uninitialised. | // I'm not sure how a function argument could be uninitialised. | ||||
| JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001) | 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 | 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 | else | ||||
| { | { | ||||
| @@ -314,6 +370,54 @@ bool MultiDocumentPanel::closeDocument (Component* component, | |||||
| JUCE_END_IGNORE_WARNINGS_MSVC | 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 | int MultiDocumentPanel::getNumDocuments() const noexcept | ||||
| { | { | ||||
| @@ -108,6 +108,7 @@ public: | |||||
| ~MultiDocumentPanel() override; | ~MultiDocumentPanel() override; | ||||
| //============================================================================== | //============================================================================== | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| /** Tries to close all the documents. | /** Tries to close all the documents. | ||||
| If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | ||||
| @@ -120,6 +121,22 @@ public: | |||||
| @see closeDocument | @see closeDocument | ||||
| */ | */ | ||||
| bool closeAllDocuments (bool checkItsOkToCloseFirst); | 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. | /** Adds a document component to the panel. | ||||
| @@ -142,6 +159,7 @@ public: | |||||
| Colour backgroundColour, | Colour backgroundColour, | ||||
| bool deleteWhenRemoved); | bool deleteWhenRemoved); | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| /** Closes one of the documents. | /** Closes one of the documents. | ||||
| If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will | ||||
| @@ -158,6 +176,25 @@ public: | |||||
| */ | */ | ||||
| bool closeDocument (Component* component, | bool closeDocument (Component* component, | ||||
| bool checkItsOkToCloseFirst); | 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. | /** Returns the number of open document windows. | ||||
| @@ -248,6 +285,7 @@ public: | |||||
| TabbedComponent* getCurrentTabbedComponent() const noexcept { return tabComponent.get(); } | 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 | /** A subclass must override this to say whether its currently ok for a document | ||||
| to be closed. | to be closed. | ||||
| @@ -269,7 +307,32 @@ public: | |||||
| @see closeDocument, FileBasedDocument::saveIfNeededAndUserAgrees() | @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. | /** Creates a new window to be used for a document. | ||||
| @@ -288,12 +351,12 @@ public: | |||||
| private: | 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; | struct TabbedComponentInternal; | ||||
| friend class MultiDocumentPanelWindow; | friend class MultiDocumentPanelWindow; | ||||
| @@ -301,6 +364,12 @@ private: | |||||
| void updateOrder(); | void updateOrder(); | ||||
| void addWindow (Component*); | 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) | 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. | /** 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 | 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: | // 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. | /** 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 | 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 | it'll show a box with just an ok button | ||||
| @returns true if the ok button was pressed, false if they pressed cancel. | @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, | static bool JUCE_CALLTYPE showNativeDialogBox (const String& title, | ||||
| const String& bodyText, | const String& bodyText, | ||||
| bool isOkCancel); | bool isOkCancel); | ||||
| @@ -142,7 +142,7 @@ public: | |||||
| */ | */ | ||||
| DialogWindow* create(); | 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 | /** Launches and runs the dialog modally, returning the status code that was | ||||
| used to terminate the modal loop. | used to terminate the modal loop. | ||||
| @@ -201,7 +201,7 @@ public: | |||||
| bool shouldBeResizable = false, | bool shouldBeResizable = false, | ||||
| bool useBottomRightCornerResizer = 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. | /** Easy way of quickly showing a dialog box containing a given component. | ||||
| Note: This method has been superseded by the DialogWindow::LaunchOptions structure, | 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 | alert window should be associated with. Depending on the look | ||||
| and feel, this might be used for positioning of the alert window. | 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, | static void JUCE_CALLTYPE showMessageBox (AlertWindow::AlertIconType iconType, | ||||
| const String& title, | const String& title, | ||||
| const String& message, | const String& message, | ||||
| @@ -113,7 +113,7 @@ public: | |||||
| ~ThreadWithProgressWindow() override; | ~ThreadWithProgressWindow() override; | ||||
| //============================================================================== | //============================================================================== | ||||
| #if JUCE_MODAL_LOOPS_PERMITTED || DOXYGEN | |||||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||||
| /** Starts the thread and waits for it to finish. | /** Starts the thread and waits for it to finish. | ||||
| This will start the thread, make the dialog box appear, and wait until either | This will start the thread, make the dialog box appear, and wait until either | ||||
| @@ -72,7 +72,7 @@ public: | |||||
| @see setChangedFlag, changed | @see setChangedFlag, changed | ||||
| */ | */ | ||||
| bool hasChangedSinceSaved() const { return changedSinceSave; } | |||||
| bool hasChangedSinceSaved() const; | |||||
| /** Called to indicate that the document has changed and needs saving. | /** Called to indicate that the document has changed and needs saving. | ||||
| @@ -98,7 +98,7 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| /** Tries to open a file. | /** 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 | 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. | a message box is shown telling the user there was an error. | ||||
| @@ -110,6 +110,22 @@ public: | |||||
| bool showMessageOnFailure, | bool showMessageOnFailure, | ||||
| bool showWaitCursor = true); | 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. | /** Asks the user for a file and tries to load it. | ||||
| This will pop up a dialog box using the title, file extension and | This will pop up a dialog box using the title, file extension and | ||||
| @@ -122,6 +138,19 @@ public: | |||||
| @see loadFrom | @see loadFrom | ||||
| */ | */ | ||||
| Result loadFromUserSpecifiedFile (bool showMessageOnFailure); | 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 | /** 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. */ | 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. | /** 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 | 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, | SaveResult save (bool askUserForFileIfNotSpecified, | ||||
| bool showMessageOnFailure); | 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 | /** 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. | it if they say yes. | ||||
| @@ -169,7 +218,31 @@ public: | |||||
| @see save, saveAs, saveAsInteractive | @see save, saveAs, saveAsInteractive | ||||
| */ | */ | ||||
| SaveResult saveIfNeededAndUserAgrees(); | 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. | /** Tries to save the document to a specified file. | ||||
| If this succeeds, it'll also change the document's internal file (as returned by | If this succeeds, it'll also change the document's internal file (as returned by | ||||
| @@ -192,7 +265,32 @@ public: | |||||
| bool askUserForFileIfNotSpecified, | bool askUserForFileIfNotSpecified, | ||||
| bool showMessageOnFailure, | bool showMessageOnFailure, | ||||
| bool showWaitCursor = true); | 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. | /** 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 | This will pop up a dialog box using the title, file extension and | ||||
| @@ -201,10 +299,26 @@ public: | |||||
| to this file. | to this file. | ||||
| @param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask | @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 | @see saveIfNeededAndUserAgrees, save, saveAs | ||||
| */ | */ | ||||
| SaveResult saveAsInteractive (bool warnAboutOverwritingExistingFiles); | 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. | /** 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() | It is changed when one of the load or save methods is used, or when setFile() | ||||
| is used to explicitly set it. | 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. | /** Sets the file that this document thinks it was loaded from. | ||||
| @@ -224,7 +338,6 @@ public: | |||||
| */ | */ | ||||
| void setFile (const File& newFile); | void setFile (const File& newFile); | ||||
| protected: | protected: | ||||
| //============================================================================== | //============================================================================== | ||||
| /** Overload this to return the title of the document. | /** Overload this to return the title of the document. | ||||
| @@ -239,11 +352,33 @@ protected: | |||||
| */ | */ | ||||
| virtual Result loadDocument (const File& file) = 0; | 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. | /** This method should try to write your document to the given file. | ||||
| @returns a Result object to indicate the whether there was an error. | @returns a Result object to indicate the whether there was an error. | ||||
| */ | */ | ||||
| virtual Result saveDocument (const File& file) = 0; | 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 | /** This is used for dialog boxes to make them open at the last folder you | ||||
| were using. | were using. | ||||
| @@ -277,21 +412,18 @@ protected: | |||||
| */ | */ | ||||
| virtual void setLastDocumentOpened (const File& file) = 0; | 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. | filename that the user is presented with in the save dialog. | ||||
| The defaultFile parameter is an initial suggestion based on what the class knows | 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 | about the current document - you can return a variation on this file with a different | ||||
| extension, etc, or just return something completely different. | extension, etc, or just return something completely different. | ||||
| */ | */ | ||||
| virtual File getSuggestedSaveAsFile (const File& defaultFile); | virtual File getSuggestedSaveAsFile (const File& defaultFile); | ||||
| #endif | |||||
| private: | 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) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileBasedDocument) | ||||
| }; | }; | ||||
| @@ -317,7 +317,7 @@ public: | |||||
| GroupAlertBehaviour groupAlertBehaviour = alertAll; | GroupAlertBehaviour groupAlertBehaviour = alertAll; | ||||
| int timeoutAfterMs = 0; /**< specifies a duration in milliseconds, after which the notification should be | 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() | WebViewKeyEquivalentResponder() | ||||
| : WebViewBase ("WebViewKeyEquivalentResponder_") | : WebViewBase ("WebViewKeyEquivalentResponder_") | ||||
| { | { | ||||
| addIvar<WebViewKeyEquivalentResponder*> ("owner"); | |||||
| addMethod (@selector (performKeyEquivalent:), performKeyEquivalent, @encode (BOOL), "@:@"); | addMethod (@selector (performKeyEquivalent:), performKeyEquivalent, @encode (BOOL), "@:@"); | ||||
| registerClass(); | registerClass(); | ||||
| } | } | ||||
| private: | private: | ||||
| static WebViewKeyEquivalentResponder* getOwner (id self) | |||||
| { | |||||
| return getIvar<WebViewKeyEquivalentResponder*> (self, "owner"); | |||||
| } | |||||
| static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) | static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) | ||||
| { | { | ||||
| NSResponder* first = [[self window] firstResponder]; | NSResponder* first = [[self window] firstResponder]; | ||||
| @@ -225,9 +219,42 @@ private: | |||||
| static void runOpenPanel (id, SEL, WKWebView*, WKOpenPanelParameters* parameters, WKFrameInfo*, | static void runOpenPanel (id, SEL, WKWebView*, WKOpenPanelParameters* parameters, WKFrameInfo*, | ||||
| void (^completionHandler)(NSArray<NSURL*>*)) | 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 | auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | ||||
| | ([parameters allowsMultipleSelection] ? FileBrowserComponent::canSelectMultipleItems : 0); | | ([parameters allowsMultipleSelection] ? FileBrowserComponent::canSelectMultipleItems : 0); | ||||
| @@ -237,24 +264,17 @@ private: | |||||
| flags |= FileBrowserComponent::canSelectDirectories; | flags |= FileBrowserComponent::canSelectDirectories; | ||||
| #endif | #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()]; | auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) results.size()]; | ||||
| for (auto& f : results) | for (auto& f : results) | ||||
| [urls addObject: [NSURL fileURLWithPath: juceStringToNS (f.getFullPathName())]]; | [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 | #endif | ||||
| }; | }; | ||||
| @@ -270,8 +290,6 @@ class WebBrowserComponent::Pimpl | |||||
| public: | public: | ||||
| Pimpl (WebBrowserComponent* owner) | Pimpl (WebBrowserComponent* owner) | ||||
| { | { | ||||
| ignoreUnused (owner); | |||||
| #if JUCE_MAC | #if JUCE_MAC | ||||
| static WebViewKeyEquivalentResponder webviewClass; | static WebViewKeyEquivalentResponder webviewClass; | ||||
| webView = (WKWebView*) webviewClass.createInstance(); | webView = (WKWebView*) webviewClass.createInstance(); | ||||
| @@ -421,20 +439,37 @@ private: | |||||
| static void runOpenPanel (id, SEL, WebView*, id<WebOpenPanelResultListener> resultListener, BOOL allowMultipleFiles) | 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; | |||||
| }); | |||||
| } | } | ||||
| }; | }; | ||||