ReaWwise

REAPER extension
Log | Files | Refs | Submodules

ImportControlsComponent.cpp (15675B)


      1 /*----------------------------------------------------------------------------------------
      2 
      3 Copyright (c) 2023 AUDIOKINETIC Inc.
      4 
      5 This file is licensed to use under the license available at:
      6 https://github.com/audiokinetic/ReaWwise/blob/main/License.txt (the "License").
      7 You may not use this file except in compliance with the License.
      8 
      9 Unless required by applicable law or agreed to in writing, software distributed
     10 under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
     11 CONDITIONS OF ANY KIND, either express or implied.  See the License for the
     12 specific language governing permissions and limitations under the License.
     13 
     14 ----------------------------------------------------------------------------------------*/
     15 
     16 #include "ImportControlsComponent.h"
     17 
     18 #include "Helpers/FileHelper.h"
     19 #include "Helpers/ImportHelper.h"
     20 #include "Model/IDs.h"
     21 #include "Theme/CustomLookAndFeel.h"
     22 
     23 #include <set>
     24 
     25 namespace AK::WwiseTransfer
     26 {
     27 	enum MessageBoxOption
     28 	{
     29 		Cancel = 0,
     30 		Continue = 1
     31 	};
     32 
     33 	namespace ImportControlsComponentConstants
     34 	{
     35 		constexpr int showSilentIncrementWarningToggleWidth = 300;
     36 		constexpr int showSilentIncrementWarningToggleHeight = 60;
     37 		constexpr int showSilentIncrementWarningToggleMarginLeft = 78;
     38 
     39 		constexpr int errorMessageWidth = 260;
     40 		constexpr int errorMessageHeight = 200;
     41 		constexpr int errorMessageMarginLeft = 75;
     42 	}; // namespace ImportControlsComponentConstants
     43 
     44 	ImportControlsComponent::ImportControlsComponent(juce::ValueTree appState,
     45 		WaapiClient& waapiClient,
     46 		DawContext& dawContext,
     47 		ApplicationProperties& applicationProperties,
     48 		const juce::String& applicationName)
     49 		: applicationState(appState)
     50 		, originalsSubfolderValid(applicationState, IDs::originalsSubfolderValid, nullptr)
     51 		, importDestinationValid(applicationState, IDs::importDestinationValid, nullptr)
     52 		, importDestination(applicationState, IDs::importDestination, nullptr)
     53 		, originalsSubFolder(applicationState, IDs::originalsSubfolder, nullptr)
     54 		, originalsFolder(applicationState, IDs::originalsFolder, nullptr)
     55 		, languageSubfolder(applicationState, IDs::languageSubfolder, nullptr)
     56 		, projectPath(applicationState, IDs::projectPath, nullptr)
     57 		, containerNameExistsOption(applicationState, IDs::containerNameExists, nullptr)
     58 		, applyTemplateOption(applicationState, IDs::applyTemplate, nullptr)
     59 		, transferInProgress(applicationState, IDs::transferInProgress, nullptr)
     60 		, hierarchyMapping(applicationState.getChildWithName(IDs::hierarchyMapping))
     61 		, previewItems(applicationState.getChildWithName(IDs::previewItems))
     62 		, waapiClient(waapiClient)
     63 		, dawContext(dawContext)
     64 		, applicationProperties(applicationProperties)
     65 		, applicationName(applicationName)
     66 	{
     67 		using namespace ImportControlsComponentConstants;
     68 
     69 		auto featureSupport = applicationState.getChildWithName(IDs::featureSupport);
     70 		selectObjectsOnImportCommand.referTo(featureSupport, IDs::selectObjectsOnImportCommand, nullptr);
     71 		applyTemplateFeatureEnabled.referTo(featureSupport, IDs::applyTemplateFeatureEnabled, nullptr);
     72 		undoGroupFeatureEnabled.referTo(featureSupport, IDs::undoGroupFeatureEnabled, nullptr);
     73 		waqlEnabled.referTo(featureSupport, IDs::waqlEnabled, nullptr);
     74 
     75 		importButton.setButtonText("Transfer to Wwise");
     76 
     77 		importButton.onClick = [this]
     78 		{
     79 			transferToWwise();
     80 		};
     81 
     82 		addAndMakeVisible(importButton);
     83 
     84 		refreshComponent();
     85 
     86 		applicationState.addListener(this);
     87 
     88 		showSilentIncrementWarningToggle.setButtonText("Don't show message again");
     89 		showSilentIncrementWarningToggle.setSize(showSilentIncrementWarningToggleWidth, showSilentIncrementWarningToggleHeight);
     90 
     91 		errorMessageContainer.setMultiLine(true);
     92 		errorMessageContainer.setReadOnly(true);
     93 		errorMessageContainer.setSize(errorMessageWidth, errorMessageHeight);
     94 	}
     95 
     96 	ImportControlsComponent::~ImportControlsComponent()
     97 	{
     98 		applicationState.removeListener(this);
     99 	}
    100 
    101 	void ImportControlsComponent::resized()
    102 	{
    103 		importButton.setBounds(getLocalBounds());
    104 	}
    105 
    106 	void ImportControlsComponent::transferToWwise()
    107 	{
    108 		using namespace ImportControlsComponentConstants;
    109 
    110 		if(transferInProgress.get())
    111 			return;
    112 
    113 		transferInProgress = true;
    114 
    115 		const auto hierarchyMappingPath =
    116 			ImportHelper::hierarchyMappingToPath(ImportHelper::valueTreeToHierarchyMappingNodeList(applicationState.getChildWithName(IDs::hierarchyMapping)));
    117 		const Import::Options opts(importDestination, originalsSubFolder, hierarchyMappingPath);
    118 
    119 		const auto previewItems = dawContext.getItemsForPreview(opts);
    120 
    121 		// Confirm that files where rendered
    122 		std::set<juce::File> directorySet;
    123 		for(const auto item : previewItems)
    124 		{
    125 			directorySet.insert(juce::File(item.audioFilePath).getParentDirectory());
    126 		}
    127 
    128 		auto lastModificationTime = juce::Time::getCurrentTime();
    129 
    130 		juce::Logger::writeToLog("Sending render request to DAW");
    131 
    132 		dawContext.renderItems();
    133 
    134 		if(FileHelper::countModifiedFilesInDirectoriesSince(directorySet, lastModificationTime) != previewItems.size())
    135 		{
    136 			onRenderFailedDetected();
    137 			return;
    138 		}
    139 
    140 		auto importItems = dawContext.getItemsForImport(opts);
    141 
    142 		bool showIncompletePathWarning = false;
    143 		bool showRenameWarning = false;
    144 		bool isCrossMachineTransferEnabled = applicationProperties.getIsCrossMachineTransferEnabled();
    145 
    146 		if(importItems.size() > 0)
    147 		{
    148 			for(auto& importItem : importItems)
    149 			{
    150 				if(importItem.renderFilePath.isEmpty())
    151 				{
    152 					onRenderFailedDetected();
    153 					return;
    154 				}
    155 
    156 				if(juce::File(importItem.audioFilePath) != juce::File(importItem.renderFilePath))
    157 					showRenameWarning = true;
    158 
    159 				if(!WwiseHelper::isPathComplete(importItem.path))
    160 					showIncompletePathWarning = true;
    161 
    162 				using namespace juce;
    163 				const File rendPath(importItem.renderFilePath);
    164 				importItem.renderFileName = rendPath.getFileName();
    165 
    166 				if(isCrossMachineTransferEnabled)
    167 				{
    168 					MemoryBlock mb;
    169 					std::unique_ptr<FileInputStream> inputStream = rendPath.createInputStream();
    170 					inputStream->readIntoMemoryBlock(mb);
    171 					importItem.renderFileWavBase64 = Base64::toBase64(mb.getData(), mb.getSize());
    172 					// add base64 padding
    173 					importItem.renderFileWavBase64 += String(std::string(importItem.renderFileWavBase64.length() % 4, '='));
    174 				}
    175 			}
    176 		}
    177 		else
    178 		{
    179 			juce::Logger::writeToLog("No items to import...");
    180 			transferInProgress = false;
    181 			return;
    182 		}
    183 
    184 		if(showRenameWarning && applicationProperties.getShowSilentIncrementWarning())
    185 			onFileRenamedDetected(showIncompletePathWarning, importItems);
    186 		else if(showIncompletePathWarning)
    187 			onPathIncompleteDetected(importItems);
    188 		else
    189 			onImport(importItems);
    190 	}
    191 
    192 	void ImportControlsComponent::showImportSummaryModal(const Import::Summary& summary, const Import::Task::Options& importTaskOptions)
    193 	{
    194 		auto hasErrors = !summary.errors.empty();
    195 
    196 		juce::String title(!hasErrors ? "Wwise Import Successful" : "Wwise Imported with Errors");
    197 
    198 		juce::String message;
    199 		message << summary.getNumObjectsCreated() << " object(s) created.";
    200 		message << juce::NewLine() << summary.getNumObjectTemplatesApplied() << " object template(s) applied.";
    201 		message << juce::NewLine() << summary.getNumAudiofilesTransfered() << " audio files(s) imported.";
    202 
    203 		auto messageBoxOptions = juce::MessageBoxOptions().withTitle(title).withMessage(message).withButton("View Details").withButton("Close");
    204 
    205 		auto onDialogBtnClicked = [this, summary = summary, importTaskOptions = importTaskOptions](int result)
    206 		{
    207 			if(result == MessageBoxOption::Continue)
    208 			{
    209 				auto importSummaryFile = createImportSummaryFile(summary, importTaskOptions);
    210 				importSummaryFile.launchInDefaultBrowser();
    211 			}
    212 		};
    213 
    214 		juce::AlertWindow::showAsync(messageBoxOptions, onDialogBtnClicked);
    215 
    216 		auto modalManager = juce::ModalComponentManager::getInstance();
    217 		auto alertWindow = dynamic_cast<juce::AlertWindow*>(modalManager->getModalComponent(0));
    218 
    219 		if(hasErrors)
    220 		{
    221 			juce::String errorMessage;
    222 
    223 			for(const auto& error : summary.errors)
    224 			{
    225 				errorMessage << "Error: `" + error.uri + "` for procedure `" + error.procedureUri + "`" << juce::NewLine() << juce::NewLine();
    226 				errorMessage << "Message: " + error.message << juce::NewLine() << juce::NewLine();
    227 			}
    228 
    229 			alertWindow->addCustomComponent(&errorMessageContainer);
    230 			auto currentBounds = errorMessageContainer.getBounds();
    231 			errorMessageContainer.setBounds(currentBounds.withX(ImportControlsComponentConstants::errorMessageMarginLeft));
    232 			errorMessageContainer.setText(errorMessage);
    233 			errorMessageContainer.setColour(juce::TextEditor::backgroundColourId, findColour(juce::AlertWindow::backgroundColourId));
    234 			errorMessageContainer.setFont(CustomLookAndFeelConstants::smallFontSize);
    235 		}
    236 		else
    237 		{
    238 			for(int i = 0; i < alertWindow->getNumChildComponents(); ++i)
    239 			{
    240 				auto* component = alertWindow->getChildComponent(i);
    241 				if(component->getName() == "Error Details")
    242 					component->setEnabled(false);
    243 			}
    244 		}
    245 	}
    246 
    247 	juce::URL ImportControlsComponent::createImportSummaryFile(const Import::Summary& summary, const Import::Task::Options& importTaskOptions)
    248 	{
    249 		auto currentTime = juce::Time::getCurrentTime();
    250 		auto hasErrors = !summary.errors.empty();
    251 
    252 		auto importSummaryFile = juce::File::getSpecialLocation(juce::File::tempDirectory)
    253 		                             .getChildFile(applicationName + "_WwiseImportSummary_" + currentTime.formatted("%Y-%m-%d_%H-%M-%S"))
    254 		                             .withFileExtension(".html");
    255 
    256 		importSummaryFile.appendText(ImportHelper::createImportSummary(applicationName, currentTime, summary, importTaskOptions));
    257 
    258 		return juce::URL(importSummaryFile.getFullPathName());
    259 	}
    260 
    261 	void ImportControlsComponent::refreshComponent()
    262 	{
    263 		auto importButtonEnabled = !transferInProgress.get() && originalsSubfolderValid.get() && importDestinationValid.get() && projectPath.get().isNotEmpty() &&
    264 		                           previewItems.getNumChildren() > 0;
    265 
    266 		auto hieararchyMappingNodes = ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping);
    267 
    268 		auto hierarchyMappingValid = true;
    269 		for(const auto& hierarchyMappingNode : hieararchyMappingNodes)
    270 		{
    271 			hierarchyMappingValid &= hierarchyMappingNode.typeValid && hierarchyMappingNode.nameValid &&
    272 			                         (!hierarchyMappingNode.propertyTemplatePathEnabled || hierarchyMappingNode.propertyTemplatePathValid);
    273 		}
    274 
    275 		importButtonEnabled &= hierarchyMappingValid;
    276 
    277 		importButton.setEnabled(importButtonEnabled);
    278 
    279 		juce::String tooltip;
    280 
    281 		if(projectPath.get().isEmpty())
    282 			tooltip = "Connect to Wwise to continue";
    283 		else if(previewItems.getNumChildren() == 0)
    284 			tooltip = "Nothing to transfer";
    285 		else if(!importButtonEnabled)
    286 			tooltip = "Fix pending errors to continue";
    287 
    288 		importButton.setTooltip(tooltip);
    289 	}
    290 
    291 	void ImportControlsComponent::valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property)
    292 	{
    293 		if(treeWhosePropertyHasChanged == applicationState && (property == IDs::originalsSubfolderValid || property == IDs::importDestinationValid ||
    294 																  property == IDs::projectPath || property == IDs::transferInProgress) ||
    295 			treeWhosePropertyHasChanged.getType() == IDs::hierarchyMappingNode)
    296 		{
    297 			triggerAsyncUpdate();
    298 		}
    299 	}
    300 
    301 	void ImportControlsComponent::valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded)
    302 	{
    303 		if(parentTree.getType() == IDs::hierarchyMapping || parentTree.getType() == IDs::previewItems)
    304 		{
    305 			triggerAsyncUpdate();
    306 		}
    307 	}
    308 
    309 	void ImportControlsComponent::valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved)
    310 	{
    311 		if(parentTree.getType() == IDs::hierarchyMapping || parentTree.getType() == IDs::previewItems)
    312 		{
    313 			triggerAsyncUpdate();
    314 		}
    315 	}
    316 
    317 	void ImportControlsComponent::valueTreeChildOrderChanged(juce::ValueTree& parentTreeWhoseChildrenHaveMoved, int oldIndex, int newIndex)
    318 	{
    319 		if(parentTreeWhoseChildrenHaveMoved.getType() == IDs::hierarchyMapping)
    320 		{
    321 			triggerAsyncUpdate();
    322 		}
    323 	}
    324 
    325 	void ImportControlsComponent::handleAsyncUpdate()
    326 	{
    327 		refreshComponent();
    328 	}
    329 
    330 	void ImportControlsComponent::onRenderFailedDetected()
    331 	{
    332 		const juce::String message("One or more files failed to render.");
    333 		juce::Logger::writeToLog(message);
    334 
    335 		juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Transfer to Wwise Aborted", message);
    336 
    337 		transferInProgress = false;
    338 		importTask.reset();
    339 	}
    340 
    341 	void ImportControlsComponent::onImportCancelled()
    342 	{
    343 		juce::Logger::writeToLog("Import was cancelled by user...");
    344 
    345 		transferInProgress = false;
    346 		importTask.reset();
    347 	}
    348 
    349 	void ImportControlsComponent::onFileRenamedDetected(bool isPathIncomplete, const std::vector<Import::Item>& importItems)
    350 	{
    351 		auto onDialogBtnClicked = [this, isPathIncomplete, importItems](int result)
    352 		{
    353 			applicationProperties.setShowSilentIncrementWarning(!showSilentIncrementWarningToggle.getToggleState());
    354 
    355 			if(result == MessageBoxOption::Continue)
    356 			{
    357 				if(isPathIncomplete)
    358 					onPathIncompleteDetected(importItems);
    359 				else
    360 					onImport(importItems);
    361 			}
    362 			else
    363 				onImportCancelled();
    364 		};
    365 
    366 		const juce::String message("One or more file names where silently incremented to avoid overwriting during the render process.");
    367 		juce::Logger::writeToLog(message);
    368 
    369 		auto messageBoxOptions = juce::MessageBoxOptions().withTitle("Action Required").withMessage(message).withButton("Continue").withButton("Cancel");
    370 
    371 		juce::AlertWindow::showAsync(messageBoxOptions, onDialogBtnClicked);
    372 
    373 		auto modalManager = juce::ModalComponentManager::getInstance();
    374 		auto alertWindow = dynamic_cast<juce::AlertWindow*>(modalManager->getModalComponent(0));
    375 		alertWindow->addCustomComponent(&showSilentIncrementWarningToggle);
    376 
    377 		// Reset and reposition the toggle button
    378 		showSilentIncrementWarningToggle.setToggleState(false, true);
    379 		auto bounds = showSilentIncrementWarningToggle.getBounds();
    380 		bounds.setX(ImportControlsComponentConstants::showSilentIncrementWarningToggleMarginLeft);
    381 		showSilentIncrementWarningToggle.setBounds(bounds);
    382 	}
    383 
    384 	void ImportControlsComponent::onPathIncompleteDetected(const std::vector<Import::Item>& importItems)
    385 	{
    386 		auto onDialogBtnClicked = [this, importItems](int result)
    387 		{
    388 			if(result == MessageBoxOption::Continue)
    389 				onImport(importItems);
    390 			else
    391 				onImportCancelled();
    392 		};
    393 
    394 		const juce::String message("One or more object paths are incomplete and will not be transfered.");
    395 		juce::Logger::writeToLog(message);
    396 
    397 		auto messageBoxOptions = juce::MessageBoxOptions().withTitle("Action Required").withMessage(message).withButton("Continue").withButton("Cancel");
    398 
    399 		juce::AlertWindow::showAsync(messageBoxOptions, onDialogBtnClicked);
    400 	}
    401 
    402 	void ImportControlsComponent::onImport(const std::vector<Import::Item>& importItems)
    403 	{
    404 		const auto hierarchyMappingNodeList = ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping);
    405 
    406 		const Import::Task::Options importTaskOptions{
    407 			importItems, containerNameExistsOption, applyTemplateOption, importDestination, hierarchyMappingNodeList,
    408 			originalsFolder, languageSubfolder, selectObjectsOnImportCommand, applyTemplateFeatureEnabled, undoGroupFeatureEnabled,
    409 			waqlEnabled};
    410 
    411 		auto onImportComplete = [this, importTaskOptions = importTaskOptions](const Import::Summary& importSummary)
    412 		{
    413 			showImportSummaryModal(importSummary, importTaskOptions);
    414 
    415 			transferInProgress = false;
    416 			importTask.reset(); // Needs to be called last since this lambda gets called inside of importTask
    417 		};
    418 
    419 		juce::Logger::writeToLog("Importing files...");
    420 
    421 		importTask.reset(new ImportTask(waapiClient, importTaskOptions, onImportComplete));
    422 		importTask->launchThread();
    423 	}
    424 
    425 } // namespace AK::WwiseTransfer