ImportPreviewComponent.cpp (13402B)
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 "ImportPreviewComponent.h" 17 18 #include "Helpers/ImportHelper.h" 19 #include "Helpers/WwiseHelper.h" 20 #include "Model/IDs.h" 21 22 #include <set> 23 #include <unordered_map> 24 25 namespace AK::WwiseTransfer 26 { 27 namespace ImportPreviewComponentConstants 28 { 29 constexpr int headerHeight = 24; 30 constexpr int columnInitialWidth = 10; 31 constexpr int columnMinimumWidth = 10; 32 constexpr int columnMaximumWidth = 1000; 33 const std::initializer_list<juce::String> tableHeaders{"Name", "Object Status", "Originals WAV", "WAV Status"}; 34 constexpr int defaultColumnPropertyFlags = juce::TableHeaderComponent::ColumnPropertyFlags::visible | juce::TableHeaderComponent::ColumnPropertyFlags::resizable | juce::TableHeaderComponent::ColumnPropertyFlags::sortable; 35 constexpr int iconPadding = 2; 36 constexpr int disabledFlag = 128; 37 } // namespace ImportPreviewComponentConstants 38 39 enum TreeValueItemColumn 40 { 41 Name = 1, 42 ObjectStatus, 43 OriginalsWav, 44 WavStatus 45 }; 46 47 ImportPreviewComponent::ImportPreviewComponent(juce::ValueTree appState) 48 : applicationState(appState) 49 , previewItems(applicationState.getChildWithName(IDs::previewItems)) 50 , rootItem(header, previewItems) 51 , previewLoading(applicationState, IDs::previewLoading, nullptr) 52 { 53 using namespace ImportPreviewComponentConstants; 54 55 auto featureSupport = appState.getChildWithName(IDs::featureSupport); 56 applyTemplateFeatureEnabled.referTo(featureSupport, IDs::applyTemplateFeatureEnabled, nullptr); 57 58 treeView.setDefaultOpenness(true); 59 treeView.setRootItem(&rootItem); 60 treeView.setRootItemVisible(false); 61 treeView.setMultiSelectEnabled(true); 62 treeView.setWantsKeyboardFocus(true); 63 treeView.addKeyListener(this); 64 65 for(const auto& tableHeader : tableHeaders) 66 { 67 auto index = (&tableHeader - tableHeaders.begin()); 68 header.addColumn(tableHeader, index + 1, columnInitialWidth, columnMinimumWidth, columnMaximumWidth, defaultColumnPropertyFlags); 69 } 70 71 header.setStretchToFitActive(true); 72 73 applicationState.addListener(this); 74 75 emptyState.setText("Current Render settings will not result in any rendered files.", juce::dontSendNotification); 76 emptyState.setJustificationType(juce::Justification::centred); 77 78 addAndMakeVisible(header); 79 addAndMakeVisible(treeView); 80 addChildComponent(loadingComponent); 81 82 refreshHeader(); 83 84 addChildComponent(emptyState); 85 } 86 87 ImportPreviewComponent::~ImportPreviewComponent() 88 { 89 applicationState.removeListener(this); 90 treeView.removeKeyListener(this); 91 } 92 93 void ImportPreviewComponent::resized() 94 { 95 auto area = getLocalBounds(); 96 97 header.setBounds(area.removeFromTop(ImportPreviewComponentConstants::headerHeight)); 98 header.resizeAllColumnsToFit(area.getWidth()); 99 100 treeView.setBounds(area); 101 102 loadingComponent.setBounds(area); 103 104 emptyState.setBounds(area); 105 } 106 107 void ImportPreviewComponent::valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, 108 const juce::Identifier& property) 109 { 110 if(property == IDs::previewLoading) 111 { 112 triggerAsyncUpdate(); 113 } 114 115 if(treeWhosePropertyHasChanged.getType() == IDs::featureSupport && property == IDs::applyTemplateFeatureEnabled) 116 { 117 refreshHeader(); 118 } 119 } 120 121 void ImportPreviewComponent::valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded) 122 { 123 if(parentTree.getType() == IDs::previewItems) 124 { 125 triggerAsyncUpdate(); 126 } 127 } 128 129 void ImportPreviewComponent::valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) 130 { 131 if(parentTree.getType() == IDs::previewItems) 132 { 133 triggerAsyncUpdate(); 134 } 135 } 136 137 void ImportPreviewComponent::handleAsyncUpdate() 138 { 139 loadingComponent.setVisible(previewLoading); 140 emptyState.setVisible(previewItems.getNumChildren() == 0 && !previewLoading); 141 } 142 143 void ImportPreviewComponent::refreshHeader() 144 { 145 using namespace ImportPreviewComponentConstants; 146 147 // For some reason, the column width measurement only works properly if the following attribute is set to false 148 header.setStretchToFitActive(false); 149 150 auto currentWidth = header.getColumnWidth(tableHeaders.size()); 151 auto propertyFlags = defaultColumnPropertyFlags; 152 153 if(!applyTemplateFeatureEnabled.get()) 154 propertyFlags |= disabledFlag; 155 156 header.removeColumn(tableHeaders.size()); 157 header.addColumn(*(tableHeaders.end() - 1), tableHeaders.size(), currentWidth, columnMinimumWidth, columnMaximumWidth, propertyFlags); 158 159 header.setStretchToFitActive(true); 160 } 161 162 void ImportPreviewComponent::copySelectedItemsToClipBoard() 163 { 164 juce::String header("Name\tObject Status\tOriginals Wav\tWav Status\r\n"); 165 166 juce::String body; 167 for(int i = 0; i < treeView.getNumSelectedItems(); ++i) 168 { 169 auto selectedItem = dynamic_cast<ValueTreeItem*>(treeView.getSelectedItem(i)); 170 171 if(selectedItem) 172 { 173 auto valueTree = selectedItem->getValueTree(); 174 175 auto previewItem = ImportHelper::valueTreeToPreviewItemNode(valueTree); 176 177 body << valueTree.getType() << "\t" << ImportHelper::objectStatusToReadableString(previewItem.objectStatus) << "\t" 178 << previewItem.audioFilePath << "\t" << ImportHelper::wavStatusToReadableString(previewItem.wavStatus) << "\r\n"; 179 } 180 } 181 182 if(body.isNotEmpty()) 183 juce::SystemClipboard::copyTextToClipboard(header << body); 184 } 185 186 bool ImportPreviewComponent::keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) 187 { 188 if(key == juce::KeyPress('c', juce::ModifierKeys::ctrlModifier, 0)) 189 { 190 copySelectedItemsToClipBoard(); 191 return true; 192 } 193 194 return false; 195 } 196 197 ValueTreeItem::ValueTreeItem(juce::TableHeaderComponent& header, juce::ValueTree t) 198 : header(header) 199 , tree(t) 200 { 201 setDrawsInLeftMargin(true); 202 203 tree.addListener(this); 204 header.addListener(this); 205 } 206 207 ValueTreeItem::~ValueTreeItem() 208 { 209 tree.removeListener(this); 210 header.removeListener(this); 211 } 212 213 bool ValueTreeItem::mightContainSubItems() 214 { 215 return tree.getNumChildren() > 0; 216 } 217 218 void ValueTreeItem::itemOpennessChanged(bool isNowOpen) 219 { 220 if(isNowOpen && getNumSubItems() == 0) 221 { 222 refreshSubItems(); 223 } 224 else 225 { 226 // TODO: there may be a performance benefit to call clear subitems. The issue with calling clearSubItems() 227 // here is that it loses the "openness" state of the tree. 228 // clearSubItems(); 229 } 230 } 231 232 void ValueTreeItem::refreshSubItems() 233 { 234 clearSubItems(); 235 236 auto isSorted = header.getSortColumnId() > 0; 237 238 if(isSorted) 239 { 240 auto comparator = Comparator(header); 241 242 for(int i = 0; i < tree.getNumChildren(); ++i) 243 { 244 addSubItemSorted(comparator, new ValueTreeItem(header, tree.getChild(i))); 245 } 246 } 247 else 248 { 249 for(int i = 0; i < tree.getNumChildren(); ++i) 250 { 251 addSubItem(new ValueTreeItem(header, tree.getChild(i))); 252 } 253 } 254 } 255 256 void ValueTreeItem::valueTreePropertyChanged(juce::ValueTree&, const juce::Identifier&) 257 { 258 repaintItem(); 259 } 260 261 void ValueTreeItem::valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree&) 262 { 263 treeChildrenChanged(parentTree); 264 } 265 266 void ValueTreeItem::valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree&, int) 267 { 268 treeChildrenChanged(parentTree); 269 } 270 271 void ValueTreeItem::valueTreeChildOrderChanged(juce::ValueTree& parentTree, int, int) 272 { 273 treeChildrenChanged(parentTree); 274 } 275 276 void ValueTreeItem::valueTreeParentChanged(juce::ValueTree&) 277 { 278 } 279 280 void ValueTreeItem::treeChildrenChanged(const juce::ValueTree& parentTree) 281 { 282 if(parentTree == tree) 283 { 284 refreshSubItems(); 285 treeHasChanged(); 286 } 287 } 288 289 void ValueTreeItem::paintItem(juce::Graphics& g, int width, int height) 290 { 291 if(isSelected()) 292 g.fillAll(juce::LookAndFeel::getDefaultLookAndFeel().findColour(juce::TextEditor::highlightColourId)); 293 294 using namespace ImportPreviewComponentConstants; 295 296 auto previewItem = ImportHelper::valueTreeToPreviewItemNode(tree); 297 298 std::array<juce::String, 4> cellText{ 299 previewItem.name, 300 ImportHelper::objectStatusToReadableString(previewItem.objectStatus), 301 previewItem.audioFilePath, 302 ImportHelper::wavStatusToReadableString(previewItem.wavStatus), 303 }; 304 305 jassert(cellText.size() == header.getNumColumns(true)); 306 307 auto* customLookAndFeel = dynamic_cast<CustomLookAndFeel*>(&getOwnerView()->getLookAndFeel()); 308 auto textColor = customLookAndFeel->getTextColourForObjectStatus(previewItem.objectStatus); 309 310 // First column is special since it has indentation + icon 311 auto indent = getItemPosition(true).getX(); 312 int iconSize = height; 313 int trueColumnWidth = header.getColumnWidth(1) - indent; 314 int textWidth = trueColumnWidth - iconSize; 315 316 auto icon = customLookAndFeel->getIconForObjectType(previewItem.type); 317 318 g.setFont(CustomLookAndFeelConstants::smallFontSize); 319 320 if(trueColumnWidth > iconSize) 321 { 322 icon->drawWithin(g, juce::Rectangle<float>(iconSize, iconSize).reduced(iconPadding, iconPadding), juce::RectanglePlacement::centred, 1); 323 324 if(textWidth > 0) 325 { 326 auto objectNameColor = previewItem.unresolvedWildcard ? getOwnerView()->getLookAndFeel().findColour(ValueTreeItem::errorOutlineColor) : textColor; 327 auto objectName = previewItem.unresolvedWildcard ? "<unresolved_wildcard>" : cellText[0]; 328 329 g.setColour(objectNameColor); 330 g.drawText(objectName, 331 iconSize, 0, textWidth, height, 332 juce::Justification::centredLeft, true); 333 } 334 } 335 336 if(!previewItem.unresolvedWildcard) 337 { 338 auto xPosition = trueColumnWidth; 339 340 g.setColour(textColor); 341 342 // The rest of the cells are just text, so no special text coordinates to calculate 343 for(int i = 1; i < cellText.size(); ++i) 344 { 345 auto columnWidth = header.getColumnWidth(i + 1); 346 347 g.drawText(cellText[i], 348 xPosition, 0, columnWidth, height, 349 juce::Justification::centredLeft, true); 350 351 xPosition += columnWidth; 352 } 353 } 354 } 355 356 void ValueTreeItem::tableColumnsChanged(juce::TableHeaderComponent* tableHeader) 357 { 358 } 359 360 void ValueTreeItem::tableColumnsResized(juce::TableHeaderComponent* tableHeader) 361 { 362 repaintItem(); 363 } 364 365 void ValueTreeItem::tableSortOrderChanged(juce::TableHeaderComponent* tableHeader) 366 { 367 if(isOpen()) 368 { 369 auto comparator = Comparator(header); 370 371 sortSubItems(comparator); 372 treeHasChanged(); 373 } 374 } 375 376 juce::String ValueTreeItem::getUniqueName() const 377 { 378 return tree.getType().toString(); 379 } 380 381 juce::String ValueTreeItem::getComparisonTextForColumn(int column) 382 { 383 auto previewItem = ImportHelper::valueTreeToPreviewItemNode(tree); 384 385 switch(column) 386 { 387 case TreeValueItemColumn::Name: 388 return getUniqueName(); 389 case TreeValueItemColumn::ObjectStatus: 390 return ImportHelper::objectStatusToReadableString(previewItem.objectStatus); 391 case TreeValueItemColumn::OriginalsWav: 392 return previewItem.audioFilePath; 393 case TreeValueItemColumn::WavStatus: 394 return ImportHelper::wavStatusToReadableString(previewItem.wavStatus); 395 default: 396 return juce::String(); 397 } 398 } 399 400 void ValueTreeItem::itemClicked(const juce::MouseEvent& event) 401 { 402 if(event.mods == juce::ModifierKeys::rightButtonModifier) 403 { 404 auto onCopy = [this] 405 { 406 auto treeView = getOwnerView(); 407 408 if(treeView) 409 { 410 auto parent = treeView->getParentComponent(); 411 412 if(parent) 413 { 414 auto importPreviewComponent = dynamic_cast<ImportPreviewComponent*>(parent); 415 416 if(importPreviewComponent) 417 { 418 importPreviewComponent->copySelectedItemsToClipBoard(); 419 return; 420 } 421 } 422 } 423 424 juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Copy Error", "Unable to add selected items to clipboard"); 425 }; 426 427 juce::PopupMenu contextMenu; 428 contextMenu.addItem("Copy to Clipboard", onCopy); 429 430 auto treeView = getOwnerView(); 431 432 if(treeView) 433 contextMenu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(treeView)); 434 } 435 } 436 437 juce::ValueTree ValueTreeItem::getValueTree() 438 { 439 return tree; 440 } 441 442 ValueTreeItem::Comparator::Comparator(const juce::TableHeaderComponent& header) 443 : sortColumnId(header.getSortColumnId() == 0 ? 1 : header.getSortColumnId()) 444 , sortDirectionForward(header.getSortColumnId() == 0 ? true : header.isSortedForwards()) 445 { 446 } 447 448 int ValueTreeItem::Comparator::compareElements(juce::TreeViewItem* first, juce::TreeViewItem* second) 449 { 450 auto firstAsValueTreeItem = dynamic_cast<ValueTreeItem*>(first); 451 auto secondAsValueTreeItem = dynamic_cast<ValueTreeItem*>(second); 452 453 if(firstAsValueTreeItem && secondAsValueTreeItem) 454 { 455 auto firstTextValue = firstAsValueTreeItem->getComparisonTextForColumn(sortColumnId); 456 auto secondTextValue = secondAsValueTreeItem->getComparisonTextForColumn(sortColumnId); 457 458 auto compareVal = firstTextValue.compareNatural(secondTextValue); 459 460 return sortDirectionForward ? compareVal : -1 * compareVal; 461 } 462 463 return 0; 464 } 465 } // namespace AK::WwiseTransfer