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