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