/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../../Application/jucer_Headers.h" #include "jucer_ProjectContentComponent.h" #include "Sidebar/jucer_Sidebar.h" struct WizardHolder { std::unique_ptr wizard; }; NewFileWizard::Type* createGUIComponentWizard (Project&); //============================================================================== ProjectContentComponent::ProjectContentComponent() : sidebar (std::make_unique (project)) { setOpaque (true); setWantsKeyboardFocus (true); addAndMakeVisible (headerComponent); addAndMakeVisible (projectMessagesComponent); addAndMakeVisible (contentViewComponent); sidebarSizeConstrainer.setMinimumWidth (200); sidebarSizeConstrainer.setMaximumWidth (500); ProjucerApplication::getApp().openDocumentManager.addListener (this); getGlobalProperties().addChangeListener (this); } ProjectContentComponent::~ProjectContentComponent() { getGlobalProperties().removeChangeListener (this); ProjucerApplication::getApp().openDocumentManager.removeListener (this); setProject (nullptr); removeChildComponent (&bubbleMessage); } void ProjectContentComponent::paint (Graphics& g) { g.fillAll (findColour (backgroundColourId)); } void ProjectContentComponent::resized() { auto r = getLocalBounds(); r.removeFromRight (10); r.removeFromLeft (15); r.removeFromTop (5); projectMessagesComponent.setBounds (r.removeFromBottom (40).withWidth (100).reduced (0, 5)); headerComponent.setBounds (r.removeFromTop (40)); r.removeFromTop (10); auto sidebarArea = r.removeFromLeft (sidebar != nullptr && sidebar->getWidth() != 0 ? sidebar->getWidth() : r.getWidth() / 4); if (sidebar != nullptr && sidebar->isVisible()) sidebar->setBounds (sidebarArea); if (resizerBar != nullptr) resizerBar->setBounds (r.withWidth (4)); contentViewComponent.setBounds (r); headerComponent.sidebarTabsWidthChanged (sidebarArea.getWidth()); } void ProjectContentComponent::lookAndFeelChanged() { repaint(); if (translationTool != nullptr) translationTool->repaint(); } void ProjectContentComponent::childBoundsChanged (Component* child) { if (child == sidebar.get()) resized(); } void ProjectContentComponent::setProject (Project* newProject) { if (project != newProject) { if (project != nullptr) project->removeChangeListener (this); hideEditor(); resizerBar = nullptr; sidebar = nullptr; project = newProject; if (project != nullptr) { sidebar = std::make_unique (project); addAndMakeVisible (sidebar.get()); //============================================================================== resizerBar = std::make_unique (sidebar.get(), &sidebarSizeConstrainer, ResizableEdgeComponent::rightEdge); addAndMakeVisible (resizerBar.get()); resizerBar->setAlwaysOnTop (true); project->addChangeListener (this); updateMissingFileStatuses(); headerComponent.setVisible (true); headerComponent.setCurrentProject (project); projectMessagesComponent.setVisible (true); } else { headerComponent.setVisible (false); projectMessagesComponent.setVisible (false); } projectMessagesComponent.setProject (project); resized(); } } void ProjectContentComponent::saveOpenDocumentList() { if (project != nullptr) { std::unique_ptr xml (recentDocumentList.createXML()); if (xml != nullptr) project->getStoredProperties().setValue ("lastDocs", xml.get()); } } void ProjectContentComponent::reloadLastOpenDocuments() { if (project != nullptr) { if (auto xml = project->getStoredProperties().getXmlValue ("lastDocs")) { recentDocumentList.restoreFromXML (*project, *xml); showDocument (recentDocumentList.getCurrentDocument(), true); } } } bool ProjectContentComponent::documentAboutToClose (OpenDocumentManager::Document* document) { hideDocument (document); return true; } void ProjectContentComponent::changeListenerCallback (ChangeBroadcaster* broadcaster) { if (broadcaster == project) updateMissingFileStatuses(); } void ProjectContentComponent::refreshProjectTreeFileStatuses() { if (sidebar != nullptr) if (auto* fileTree = sidebar->getFileTreePanel()) fileTree->repaint(); } void ProjectContentComponent::updateMissingFileStatuses() { if (sidebar != nullptr) if (auto* tree = sidebar->getFileTreePanel()) tree->updateMissingFileStatuses(); } bool ProjectContentComponent::showEditorForFile (const File& fileToShow, bool grabFocus) { if (getCurrentFile() != fileToShow) return showDocument (ProjucerApplication::getApp().openDocumentManager.openFile (project, fileToShow), grabFocus); return true; } bool ProjectContentComponent::hasFileInRecentList (const File& f) const { return recentDocumentList.contains (f); } File ProjectContentComponent::getCurrentFile() const { return currentDocument != nullptr ? currentDocument->getFile() : File(); } bool ProjectContentComponent::showDocument (OpenDocumentManager::Document* doc, bool grabFocus) { if (doc == nullptr) return false; if (doc->hasFileBeenModifiedExternally()) doc->reloadFromFile(); if (doc != getCurrentDocument()) { recentDocumentList.newDocumentOpened (doc); setEditorDocument (doc->createEditor(), doc); } if (grabFocus && contentViewComponent.isShowing()) contentViewComponent.grabKeyboardFocus(); return true; } void ProjectContentComponent::hideEditor() { currentDocument = nullptr; contentViewComponent.setContent ({}, {}); ProjucerApplication::getCommandManager().commandStatusChanged(); resized(); } void ProjectContentComponent::hideDocument (OpenDocumentManager::Document* doc) { if (doc != currentDocument) return; if (auto* replacement = recentDocumentList.getClosestPreviousDocOtherThan (currentDocument)) showDocument (replacement, true); else hideEditor(); } void ProjectContentComponent::setScrollableEditorComponent (std::unique_ptr component) { jassert (component.get() != nullptr); class ContentViewport : public Component { public: ContentViewport (std::unique_ptr content) { contentViewport.setViewedComponent (content.release(), true); addAndMakeVisible (contentViewport); } void resized() override { contentViewport.setBounds (getLocalBounds()); } private: Viewport contentViewport; }; contentViewComponent.setContent (std::make_unique (std::move (component)), {}); currentDocument = nullptr; ProjucerApplication::getCommandManager().commandStatusChanged(); } void ProjectContentComponent::setEditorDocument (std::unique_ptr component, OpenDocumentManager::Document* doc) { currentDocument = doc; contentViewComponent.setContent (std::move (component), currentDocument != nullptr ? currentDocument->getFile().getFileName() : String()); ProjucerApplication::getCommandManager().commandStatusChanged(); } Component* ProjectContentComponent::getEditorComponent() { return contentViewComponent.getCurrentComponent(); } void ProjectContentComponent::closeDocument() { if (currentDocument != nullptr) { ProjucerApplication::getApp().openDocumentManager .closeDocumentAsync (currentDocument, OpenDocumentManager::SaveIfNeeded::yes, nullptr); return; } if (! goToPreviousFile()) hideEditor(); } static void showSaveWarning (OpenDocumentManager::Document* currentDocument) { AlertWindow::showMessageBoxAsync (MessageBoxIconType::WarningIcon, TRANS("Save failed!"), TRANS("Couldn't save the file:") + "\n" + currentDocument->getFile().getFullPathName()); } void ProjectContentComponent::saveDocumentAsync() { if (currentDocument != nullptr) { currentDocument->saveAsync ([parent = SafePointer { this }] (bool savedSuccessfully) { if (parent == nullptr) return; if (! savedSuccessfully) showSaveWarning (parent->currentDocument); parent->refreshProjectTreeFileStatuses(); }); } else { saveProjectAsync(); } } void ProjectContentComponent::saveAsAsync() { if (currentDocument != nullptr) { currentDocument->saveAsAsync ([parent = SafePointer { this }] (bool savedSuccessfully) { if (parent == nullptr) return; if (! savedSuccessfully) showSaveWarning (parent->currentDocument); parent->refreshProjectTreeFileStatuses(); }); } } bool ProjectContentComponent::goToPreviousFile() { auto* doc = recentDocumentList.getCurrentDocument(); if (doc == nullptr || doc == getCurrentDocument()) doc = recentDocumentList.getPrevious(); return showDocument (doc, true); } bool ProjectContentComponent::goToNextFile() { return showDocument (recentDocumentList.getNext(), true); } bool ProjectContentComponent::canGoToCounterpart() const { return currentDocument != nullptr && currentDocument->getCounterpartFile().exists(); } bool ProjectContentComponent::goToCounterpart() { if (currentDocument != nullptr) { auto file = currentDocument->getCounterpartFile(); if (file.exists()) return showEditorForFile (file, true); } return false; } void ProjectContentComponent::saveProjectAsync() { if (project != nullptr) project->saveAsync (true, true, nullptr); } void ProjectContentComponent::closeProject() { if (auto* mw = findParentComponentOfClass()) mw->closeCurrentProject (OpenDocumentManager::SaveIfNeeded::yes, nullptr); } void ProjectContentComponent::showProjectSettings() { setScrollableEditorComponent (std::make_unique (*project)); } void ProjectContentComponent::showCurrentExporterSettings() { if (auto selected = headerComponent.getSelectedExporter()) showExporterSettings (selected->getUniqueName()); } void ProjectContentComponent::showExporterSettings (const String& exporterName) { if (exporterName.isEmpty()) return; showExportersPanel(); if (sidebar == nullptr) return; if (auto* exportersPanel = sidebar->getExportersTreePanel()) { if (auto* exporters = dynamic_cast (exportersPanel->rootItem.get())) { for (auto i = exporters->getNumSubItems(); i >= 0; --i) { if (auto* e = dynamic_cast (exporters->getSubItem (i))) { if (e->getDisplayName() == exporterName) { if (e->isSelected()) e->setSelected (false, true); e->setSelected (true, true); } } } } } } void ProjectContentComponent::showModule (const String& moduleID) { showModulesPanel(); if (sidebar == nullptr) return; if (auto* modsPanel = sidebar->getModuleTreePanel()) { if (auto* mods = dynamic_cast (modsPanel->rootItem.get())) { for (auto i = mods->getNumSubItems(); --i >= 0;) { if (auto* m = dynamic_cast (mods->getSubItem (i))) { if (m->moduleID == moduleID) { if (m->isSelected()) m->setSelected (false, true); m->setSelected (true, true); } } } } } } StringArray ProjectContentComponent::getExportersWhichCanLaunch() const { StringArray s; if (project != nullptr) for (Project::ExporterIterator exporter (*project); exporter.next();) if (exporter->canLaunchProject()) s.add (exporter->getUniqueName()); return s; } void ProjectContentComponent::openInSelectedIDE (bool saveFirst) { if (project == nullptr) return; if (auto selectedExporter = headerComponent.getSelectedExporter()) { if (saveFirst) { SafePointer safeThis { this }; project->saveAsync (true, true, [safeThis] (Project::SaveResult r) { if (safeThis != nullptr && r == Project::SaveResult::savedOk) safeThis->openInSelectedIDE (false); }); return; } project->openProjectInIDE (*selectedExporter); } } void ProjectContentComponent::showNewExporterMenu() { if (project != nullptr) { PopupMenu menu; menu.addSectionHeader ("Create a new export target:"); SafePointer safeThis (this); for (auto& exporterInfo : ProjectExporter::getExporterTypeInfos()) { PopupMenu::Item item; item.itemID = -1; item.text = exporterInfo.displayName; item.image = [exporterInfo] { auto drawableImage = std::make_unique(); drawableImage->setImage (exporterInfo.icon); return drawableImage; }(); item.action = [safeThis, exporterInfo] { if (safeThis != nullptr) if (auto* p = safeThis->getProject()) p->addNewExporter (exporterInfo.identifier); }; menu.addItem (item); } menu.showMenuAsync ({}); } } void ProjectContentComponent::deleteSelectedTreeItems() { if (sidebar != nullptr) if (auto* tree = sidebar->getTreeWithSelectedItems()) tree->deleteSelectedItems(); } void ProjectContentComponent::showBubbleMessage (Rectangle pos, const String& text) { addChildComponent (bubbleMessage); bubbleMessage.setColour (BubbleComponent::backgroundColourId, Colours::white.withAlpha (0.7f)); bubbleMessage.setColour (BubbleComponent::outlineColourId, Colours::black.withAlpha (0.8f)); bubbleMessage.setAlwaysOnTop (true); bubbleMessage.showAt (pos, AttributedString (text), 3000, true, false); } //============================================================================== void ProjectContentComponent::showTranslationTool() { if (translationTool != nullptr) { translationTool->toFront (true); } else if (project != nullptr) { new FloatingToolWindow ("Translation File Builder", "transToolWindowPos", new TranslationToolComponent(), translationTool, true, 600, 700, 600, 400, 10000, 10000); } } //============================================================================== struct AsyncCommandRetrier : public Timer { AsyncCommandRetrier (const ApplicationCommandTarget::InvocationInfo& i) : info (i) { info.originatingComponent = nullptr; startTimer (500); } void timerCallback() override { stopTimer(); ProjucerApplication::getCommandManager().invoke (info, true); delete this; } ApplicationCommandTarget::InvocationInfo info; JUCE_DECLARE_NON_COPYABLE (AsyncCommandRetrier) }; static bool reinvokeCommandAfterCancellingModalComps (const ApplicationCommandTarget::InvocationInfo& info) { if (ModalComponentManager::getInstance()->cancelAllModalComponents()) { new AsyncCommandRetrier (info); return true; } return false; } //============================================================================== ApplicationCommandTarget* ProjectContentComponent::getNextCommandTarget() { return findFirstTargetParentComponent(); } void ProjectContentComponent::getAllCommands (Array & commands) { commands.addArray ({ CommandIDs::saveProject, CommandIDs::closeProject, CommandIDs::saveDocument, CommandIDs::saveDocumentAs, CommandIDs::closeDocument, CommandIDs::goToPreviousDoc, CommandIDs::goToNextDoc, CommandIDs::goToCounterpart, CommandIDs::showProjectSettings, CommandIDs::showFileExplorerPanel, CommandIDs::showModulesPanel, CommandIDs::showExportersPanel, CommandIDs::showExporterSettings, CommandIDs::openInIDE, CommandIDs::saveAndOpenInIDE, CommandIDs::createNewExporter, CommandIDs::deleteSelectedItem, CommandIDs::showTranslationTool, CommandIDs::addNewGUIFile }); } void ProjectContentComponent::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result) { String documentName; if (currentDocument != nullptr) documentName = " '" + currentDocument->getName().substring (0, 32) + "'"; #if JUCE_MAC auto cmdCtrl = (ModifierKeys::ctrlModifier | ModifierKeys::commandModifier); #else auto cmdCtrl = (ModifierKeys::ctrlModifier | ModifierKeys::altModifier); #endif switch (commandID) { case CommandIDs::saveProject: result.setInfo ("Save Project", "Saves the current project", CommandCategories::general, 0); result.setActive (project != nullptr && ! project->isSaveAndExportDisabled() && ! project->isCurrentlySaving()); result.defaultKeypresses.add ({ 'p', ModifierKeys::commandModifier, 0 }); break; case CommandIDs::closeProject: result.setInfo ("Close Project", "Closes the current project", CommandCategories::general, 0); result.setActive (project != nullptr); break; case CommandIDs::saveDocument: result.setInfo ("Save" + documentName, "Saves the current document", CommandCategories::general, 0); result.setActive (currentDocument != nullptr || (project != nullptr && ! project->isCurrentlySaving())); result.defaultKeypresses.add ({ 's', ModifierKeys::commandModifier, 0 }); break; case CommandIDs::saveDocumentAs: result.setInfo ("Save As...", "Saves the current document to a new location", CommandCategories::general, 0); result.setActive (currentDocument != nullptr); result.defaultKeypresses.add ({ 's', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0 }); break; case CommandIDs::closeDocument: result.setInfo ("Close" + documentName, "Closes the current document", CommandCategories::general, 0); result.setActive (currentDocument != nullptr); result.defaultKeypresses.add ({ 'w', cmdCtrl, 0 }); break; case CommandIDs::goToPreviousDoc: result.setInfo ("Previous Document", "Go to previous document", CommandCategories::general, 0); result.setActive (recentDocumentList.canGoToPrevious()); result.defaultKeypresses.add ({ KeyPress::leftKey, cmdCtrl, 0 }); break; case CommandIDs::goToNextDoc: result.setInfo ("Next Document", "Go to next document", CommandCategories::general, 0); result.setActive (recentDocumentList.canGoToNext()); result.defaultKeypresses.add ({ KeyPress::rightKey, cmdCtrl, 0 }); break; case CommandIDs::goToCounterpart: result.setInfo ("Open Counterpart File", "Open corresponding header or cpp file", CommandCategories::general, 0); result.setActive (canGoToCounterpart()); result.defaultKeypresses.add ({ KeyPress::upKey, cmdCtrl, 0 }); break; case CommandIDs::showProjectSettings: result.setInfo ("Show Project Settings", "Shows the main project options page", CommandCategories::general, 0); result.setActive (project != nullptr); result.defaultKeypresses.add ({ 'x', cmdCtrl, 0 }); break; case CommandIDs::showFileExplorerPanel: result.setInfo ("Show File Explorer Panel", "Shows the panel containing the tree of files for this project", CommandCategories::general, 0); result.setActive (project != nullptr); result.defaultKeypresses.add ({ 'f', cmdCtrl, 0 }); break; case CommandIDs::showModulesPanel: result.setInfo ("Show Modules Panel", "Shows the panel containing the project's list of modules", CommandCategories::general, 0); result.setActive (project != nullptr); result.defaultKeypresses.add ({ 'm', cmdCtrl, 0 }); break; case CommandIDs::showExportersPanel: result.setInfo ("Show Exporters Panel", "Shows the panel containing the project's list of exporters", CommandCategories::general, 0); result.setActive (project != nullptr); result.defaultKeypresses.add ({ 'e', cmdCtrl, 0 }); break; case CommandIDs::showExporterSettings: result.setInfo ("Show Exporter Settings", "Shows the settings page for the currently selected exporter", CommandCategories::general, 0); result.setActive (project != nullptr); result.defaultKeypresses.add ({ 'e', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0 }); break; case CommandIDs::openInIDE: result.setInfo ("Open in IDE...", "Launches the project in an external IDE", CommandCategories::general, 0); result.setActive (ProjectExporter::canProjectBeLaunched (project) && ! project->isSaveAndExportDisabled()); break; case CommandIDs::saveAndOpenInIDE: result.setInfo ("Save Project and Open in IDE...", "Saves the project and launches it in an external IDE", CommandCategories::general, 0); result.setActive (ProjectExporter::canProjectBeLaunched (project) && ! project->isSaveAndExportDisabled() && ! project->isCurrentlySaving()); result.defaultKeypresses.add ({ 'l', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0 }); break; case CommandIDs::createNewExporter: result.setInfo ("Create New Exporter...", "Creates a new exporter for a compiler type", CommandCategories::general, 0); result.setActive (project != nullptr); break; case CommandIDs::deleteSelectedItem: result.setInfo ("Delete Selected File", String(), CommandCategories::general, 0); result.defaultKeypresses.add ({ KeyPress::deleteKey, 0, 0 }); result.defaultKeypresses.add ({ KeyPress::backspaceKey, 0, 0 }); break; case CommandIDs::showTranslationTool: result.setInfo ("Translation File Builder", "Shows the translation file helper tool", CommandCategories::general, 0); break; case CommandIDs::addNewGUIFile: result.setInfo ("Add new GUI Component...", "Adds a new GUI Component file to the project", CommandCategories::general, (! ProjucerApplication::getApp().isGUIEditorEnabled() ? ApplicationCommandInfo::isDisabled : 0)); break; default: break; } } 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()) return false; switch (info.commandID) { case CommandIDs::saveProject: case CommandIDs::closeProject: case CommandIDs::saveDocument: case CommandIDs::saveDocumentAs: case CommandIDs::closeDocument: case CommandIDs::goToPreviousDoc: case CommandIDs::goToNextDoc: case CommandIDs::goToCounterpart: case CommandIDs::saveAndOpenInIDE: if (reinvokeCommandAfterCancellingModalComps (info)) { grabKeyboardFocus(); // to force any open labels to close their text editors return true; } break; default: break; } if (isCurrentlyBlockedByAnotherModalComponent()) return false; switch (info.commandID) { 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; case CommandIDs::showModulesPanel: showModulesPanel(); break; case CommandIDs::showExportersPanel: showExportersPanel(); break; case CommandIDs::showExporterSettings: showCurrentExporterSettings(); break; case CommandIDs::openInIDE: openInSelectedIDE (false); break; case CommandIDs::saveAndOpenInIDE: openInSelectedIDE (true); break; case CommandIDs::createNewExporter: showNewExporterMenu(); break; case CommandIDs::deleteSelectedItem: deleteSelectedTreeItems(); break; case CommandIDs::showTranslationTool: showTranslationTool(); break; case CommandIDs::addNewGUIFile: addNewGUIFile(); break; default: return false; } return true; } bool ProjectContentComponent::isSaveCommand (const CommandID id) { return (id == CommandIDs::saveProject || id == CommandIDs::saveDocument || id == CommandIDs::saveAndOpenInIDE); } void ProjectContentComponent::getSelectedProjectItemsBeingDragged (const DragAndDropTarget::SourceDetails& dragSourceDetails, OwnedArray& selectedNodes) { TreeItemTypes::FileTreeItemBase::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); } void ProjectContentComponent::addNewGUIFile() { if (project != nullptr) { wizardHolder = std::make_unique(); wizardHolder->wizard.reset (createGUIComponentWizard (*project)); wizardHolder->wizard->createNewFile (*project, project->getMainGroup()); } } //============================================================================== void ProjectContentComponent::showProjectPanel (const int index) { if (sidebar != nullptr) sidebar->showPanel (index); }