diff --git a/CMakeLists.txt b/CMakeLists.txt index c59b55d6..f7bb6e12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ set(OSsources src/program/OutfitStudio.cpp src/program/ShapeProperties.cpp src/program/ConvertBodyReferenceDialog.cpp + src/program/SliderDataImportDialog.cpp ) set(BSsources ${commonsources} diff --git a/OutfitStudio.vcxproj b/OutfitStudio.vcxproj index 5c36da4c..edf06d01 100644 --- a/OutfitStudio.vcxproj +++ b/OutfitStudio.vcxproj @@ -758,6 +758,7 @@ + @@ -830,6 +831,7 @@ + @@ -846,6 +848,7 @@ + diff --git a/OutfitStudio.vcxproj.filters b/OutfitStudio.vcxproj.filters index ba4eed32..94211c3a 100644 --- a/OutfitStudio.vcxproj.filters +++ b/OutfitStudio.vcxproj.filters @@ -1762,6 +1762,9 @@ Utilities + + Program + @@ -1971,10 +1974,16 @@ Program + + Program + Resources + + Resources + \ No newline at end of file diff --git a/res/xrc/SliderDataImport.xrc b/res/xrc/SliderDataImport.xrc new file mode 100644 index 00000000..a5f60db3 --- /dev/null +++ b/res/xrc/SliderDataImport.xrc @@ -0,0 +1,115 @@ + + + + + 650,500 + Slider Data Import Options... + 1 + + wxVERTICAL + + + wxEXPAND + 5 + + wxHORIZONTAL + + + wxALL + 5 + + + + + + + wxEXPAND + 5 + 0,0 + + + + + + wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT + 5 + 300,80 + + + + + + + wxEXPAND + 5 + + wxHORIZONTAL + + + wxALL + 5 + + + + + + + wxEXPAND + 5 + 0,0 + + + + + + wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT + 5 + 300,200 + + + + + + + wxALL|wxEXPAND + 5 + + + 1 + + + + + wxEXPAND|wxALL + 10 + + + wxALIGN_CENTER_HORIZONTAL|wxALL + 5 + + + + + + wxALIGN_CENTER_HORIZONTAL|wxALL + 5 + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/SliderSet.h b/src/components/SliderSet.h index 0b97019d..a79cc58d 100644 --- a/src/components/SliderSet.h +++ b/src/components/SliderSet.h @@ -145,6 +145,16 @@ class SliderSet { return outDataNames; } + std::string SliderFromDataName(const std::string& targetName, const std::string& dataName) { + for (auto& s : sliders) { + for (auto& df : s.dataFiles) { + if (df.targetName == targetName && df.dataName == dataName) + return s.name; + } + } + return ""; + } + std::string TargetToShape(const std::string& targetName) { for (auto& s : shapeAttributes) if (s.second.targetShape == targetName) diff --git a/src/program/OutfitProject.cpp b/src/program/OutfitProject.cpp index 31037ce6..16e1a760 100644 --- a/src/program/OutfitProject.cpp +++ b/src/program/OutfitProject.cpp @@ -1235,6 +1235,14 @@ const std::string& OutfitProject::ShapeToTarget(const std::string& shapeName) { return shapeName; } +const std::string& OutfitProject::TargetToShape(const std::string& targetName) { + for (auto it = activeSet.ShapesBegin(); it != activeSet.ShapesEnd(); ++it) + if (it->second.targetShape == targetName) + return it->first; + + return targetName; +} + size_t OutfitProject::GetActiveBoneCount() { return AnimSkeleton::getInstance().GetActiveBoneCount(); } diff --git a/src/program/OutfitProject.h b/src/program/OutfitProject.h index 98b3cc52..3fe1f9de 100644 --- a/src/program/OutfitProject.h +++ b/src/program/OutfitProject.h @@ -160,6 +160,7 @@ class OutfitProject { void ConformShape(nifly::NiShape* shape, const ConformOptions& options = ConformOptions()); const std::string& ShapeToTarget(const std::string& shapeName); + const std::string& TargetToShape(const std::string& targetName); int GetVertexCount(nifly::NiShape* shape); void GetLiveVerts(nifly::NiShape* shape, std::vector& outVerts, std::vector* outUVs = nullptr); void GetSliderDiff(nifly::NiShape* shape, const std::string& sliderName, std::vector& outVerts); diff --git a/src/program/OutfitStudio.cpp b/src/program/OutfitStudio.cpp index d8ae5f5d..7f024fd3 100644 --- a/src/program/OutfitStudio.cpp +++ b/src/program/OutfitStudio.cpp @@ -26,6 +26,7 @@ along with this program. If not, see . #include "GroupManager.h" #include "PresetSaveDialog.h" #include "ShapeProperties.h" +#include "SliderDataImportDialog.h" #include #include @@ -7092,11 +7093,6 @@ void OutfitStudioFrame::OnSliderImportOSD(wxCommandEvent& WXUNUSED(event)) { if (fn.IsEmpty()) return; - wxMessageDialog dlg(this, _("This will delete all loaded sliders. Are you sure?"), _("OSD Import"), wxOK | wxCANCEL | wxICON_WARNING | wxCANCEL_DEFAULT); - dlg.SetOKCancelLabels(_("Import"), _("Cancel")); - if (dlg.ShowModal() != wxID_OK) - return; - wxLogMessage("Importing morphs from OSD file '%s'...", fn); OSDataFile osd; @@ -7106,52 +7102,103 @@ void OutfitStudioFrame::OnSliderImportOSD(wxCommandEvent& WXUNUSED(event)) { return; } - // Deleting sliders + std::unordered_map> shapeToSliders; + auto diffs = osd.GetDataDiffs(); + const auto& shapes = project->GetWorkNif()->GetShapes(); + for (auto& diff : diffs) { + std::string bestTargetName; + for (auto& shape : shapes) { + std::string shapeName = shape->name.get(); + std::string targetName = project->ShapeToTarget(shapeName); + + // Diff name is supposed to begin with matching shape name + if (diff.first.substr(0, targetName.size()) != targetName) + continue; + + if (shapeName.length() > bestTargetName.length()) + bestTargetName = targetName; + } + + if (bestTargetName.length() == 0) + continue; + + // Find slider name from data name + auto sliderName = project->activeSet.SliderFromDataName(bestTargetName, diff.first); + if (sliderName.empty()) + sliderName = diff.first.substr(bestTargetName.length(), diff.first.length() - bestTargetName.length() + 1); + + shapeToSliders[bestTargetName].emplace(sliderName, diff.first); + } + + SliderDataImportDialog import(this, project, OutfitStudioConfig); + if (import.ShowModal(shapeToSliders) != wxID_OK) + return; + + const auto& options = import.GetOptions(); + sliderScroll->Freeze(); - std::vector erase; - for (auto& sliderPanel : sliderPanels) { - sliderPanel.second->slider->SetValue(0); - SetSliderValue(sliderPanel.first, 0); - ShowSliderEffect(sliderPanel.first, true); - sliderPanel.second->slider->SetFocus(); - HideSliderPanel(sliderPanel.second); + if (!options.mergeSliders) { + wxMessageDialog dlg(this, _("This will delete all loaded sliders. Are you sure?"), _("OSD Import"), wxOK | wxCANCEL | wxICON_WARNING | wxCANCEL_DEFAULT); + dlg.SetOKCancelLabels(_("Import"), _("Cancel")); + if (dlg.ShowModal() != wxID_OK) { + sliderScroll->Thaw(); + return; + } + + // Deleting sliders + std::vector erase; + for (auto& sliderPanel : sliderPanels) { + sliderPanel.second->slider->SetValue(0); + SetSliderValue(sliderPanel.first, 0); + ShowSliderEffect(sliderPanel.first, true); + sliderPanel.second->slider->SetFocus(); + HideSliderPanel(sliderPanel.second); - erase.push_back(sliderPanel.first); - project->DeleteSlider(sliderPanel.first); + erase.push_back(sliderPanel.first); + project->DeleteSlider(sliderPanel.first); + } + + for (auto& e : erase) + sliderPanels.erase(e); + + MenuExitSliderEdit(); + sliderScroll->FitInside(); + activeSlider.clear(); + lastActiveSlider.clear(); } - for (auto& e : erase) - sliderPanels.erase(e); + std::unordered_set addedShapes; - MenuExitSliderEdit(); - sliderScroll->FitInside(); - activeSlider.clear(); - lastActiveSlider.clear(); + for (auto& shape : shapes) { - wxString addedDiffs; - auto diffs = osd.GetDataDiffs(); + // check if the shape is selected + auto selectedSliders = options.selectedShapesToSliders.find(shape->name.get()); + if (selectedSliders == options.selectedShapesToSliders.end()) + continue; - for (auto& shape : project->GetWorkNif()->GetShapes()) { - std::string s = shape->name.get(); - bool added = false; - for (auto& diff : diffs) { - // Diff name is supposed to begin with matching shape name - if (diff.first.substr(0, s.size()) != s) + addedShapes.emplace(shape); + + for (auto& diff : diffs) { + auto& sliderNameToDisplayName = selectedSliders->second; + + // check the diff is selected for the specific shape + auto sliderName = selectedSliders->second.find(diff.first); + if (sliderName == sliderNameToDisplayName.end()) continue; - std::string diffName = diff.first.substr(s.length(), diff.first.length() - s.length() + 1); - if (!project->ValidSlider(diffName)) { - createSliderGUI(diffName, sliderScroll, sliderScroll->GetSizer()); - project->AddEmptySlider(diffName); - ShowSliderEffect(diffName); + if (!project->ValidSlider(sliderName->second)) { + createSliderGUI(sliderName->second, sliderScroll, sliderScroll->GetSizer()); + project->AddEmptySlider(sliderName->second); + ShowSliderEffect(sliderName->second); } - project->SetSliderFromDiff(diffName, shape, diff.second); - added = true; + project->SetSliderFromDiff(sliderName->second, shape, diff.second); } + } - if (added) - addedDiffs += s + "\n"; + wxString addedDiffs; + for (auto& addedShape : addedShapes) { + addedDiffs += addedShape->name.get() + "\n"; } sliderScroll->FitInside(); @@ -7175,11 +7222,6 @@ void OutfitStudioFrame::OnSliderImportTRI(wxCommandEvent& WXUNUSED(event)) { if (fn.IsEmpty()) return; - wxMessageDialog dlg(this, _("This will delete all loaded sliders. Are you sure?"), _("TRI Import"), wxOK | wxCANCEL | wxICON_WARNING | wxCANCEL_DEFAULT); - dlg.SetOKCancelLabels(_("Import"), _("Cancel")); - if (dlg.ShowModal() != wxID_OK) - return; - wxLogMessage("Importing morphs from TRI file '%s'...", fn); TriFile tri; @@ -7189,37 +7231,73 @@ void OutfitStudioFrame::OnSliderImportTRI(wxCommandEvent& WXUNUSED(event)) { return; } - // Deleting sliders - sliderScroll->Freeze(); - std::vector erase; - for (auto& sliderPanel : sliderPanels) { - sliderPanel.second->slider->SetValue(0); - SetSliderValue(sliderPanel.first, 0); - ShowSliderEffect(sliderPanel.first, true); - sliderPanel.second->slider->SetFocus(); - HideSliderPanel(sliderPanel.second); + std::unordered_map> shapeToSliders; + auto morphs = tri.GetMorphs(); + for(auto& morph : morphs) { + auto shape = project->GetWorkNif()->FindBlockByName(morph.first); + if (!shape) + continue; - erase.push_back(sliderPanel.first); - project->DeleteSlider(sliderPanel.first); + for (auto& morphData : morph.second) + shapeToSliders[shape->name.get()].emplace(morphData->name, morphData->name); } - for (auto& e : erase) - sliderPanels.erase(e); + SliderDataImportDialog import(this, project, OutfitStudioConfig); + if (import.ShowModal(shapeToSliders) != wxID_OK) + return; - MenuExitSliderEdit(); - sliderScroll->FitInside(); - activeSlider.clear(); - lastActiveSlider.clear(); + const auto& options = import.GetOptions(); + + sliderScroll->Freeze(); + if (!options.mergeSliders) { + wxMessageDialog dlg(this, _("This will delete all loaded sliders. Are you sure?"), _("TRI Import"), wxOK | wxCANCEL | wxICON_WARNING | wxCANCEL_DEFAULT); + dlg.SetOKCancelLabels(_("Import"), _("Cancel")); + if (dlg.ShowModal() != wxID_OK) { + sliderScroll->Thaw(); + return; + } + + // Deleting sliders + std::vector erase; + for (auto& sliderPanel : sliderPanels) { + sliderPanel.second->slider->SetValue(0); + SetSliderValue(sliderPanel.first, 0); + ShowSliderEffect(sliderPanel.first, true); + sliderPanel.second->slider->SetFocus(); + HideSliderPanel(sliderPanel.second); + + erase.push_back(sliderPanel.first); + project->DeleteSlider(sliderPanel.first); + } + + for (auto& e : erase) + sliderPanels.erase(e); + + MenuExitSliderEdit(); + sliderScroll->FitInside(); + activeSlider.clear(); + lastActiveSlider.clear(); + } wxString addedMorphs; - auto morphs = tri.GetMorphs(); + std::unordered_set addedShapes; + for (auto& morph : morphs) { auto shape = project->GetWorkNif()->FindBlockByName(morph.first); if (!shape) continue; + // check if the shape is selected + auto selectedSliders = options.selectedShapesToSliders.find(shape->name.get()); + if (selectedSliders == options.selectedShapesToSliders.end()) + continue; + addedMorphs += morph.first + "\n"; for (auto& morphData : morph.second) { + // check the morph is selected for the specific shape + if (selectedSliders->second.find(morphData->name) == selectedSliders->second.end()) + continue; + if (!project->ValidSlider(morphData->name)) { createSliderGUI(morphData->name, sliderScroll, sliderScroll->GetSizer()); project->AddEmptySlider(morphData->name); diff --git a/src/program/SliderDataImportDialog.cpp b/src/program/SliderDataImportDialog.cpp new file mode 100644 index 00000000..f6635aa3 --- /dev/null +++ b/src/program/SliderDataImportDialog.cpp @@ -0,0 +1,134 @@ +/* +BodySlide and Outfit Studio +See the included LICENSE file +*/ + +#include "SliderDataImportDialog.h" + +#include "OutfitProject.h" +#include "../utils/ConfigurationManager.h" +#include "../utils/ConfigDialogUtil.h" + +extern ConfigurationManager Config; + +wxBEGIN_EVENT_TABLE(SliderDataImportDialog, wxDialog) + EVT_BUTTON(wxID_OK, SliderDataImportDialog::OnImport) +wxEND_EVENT_TABLE() + +SliderDataImportDialog::SliderDataImportDialog(wxWindow* parent, OutfitProject* project, ConfigurationManager& outfitStudioConfig) + : project(project), outfitStudioConfig(outfitStudioConfig) { + + wxXmlResource* xrc = wxXmlResource::Get(); + xrc->Load(wxString::FromUTF8(Config["AppDir"]) + "/res/xrc/SliderDataImport.xrc"); + xrc->LoadDialog(this, parent, "dlgSliderDataImport"); + ConfigDialogUtil::LoadDialogCheckBox(outfitStudioConfig, *this, "chkImportMergeSliders"); + + shapesCheckListBox = XRCCTRL(*this, "sliderShapesImportList", wxCheckListBox); + shapesCheckListBox->Bind(wxEVT_CHECKLISTBOX, &SliderDataImportDialog::OnShapeSelectionChanged, this); + + slidersCheckListBox = XRCCTRL(*this, "sliderImportList", wxCheckListBox); + slidersCheckListBox->Bind(wxEVT_RIGHT_UP, &SliderDataImportDialog::OnSliderListContext, this); + + SetDoubleBuffered(true); + CenterOnParent(); +} + +SliderDataImportDialog::~SliderDataImportDialog() { + wxXmlResource::Get()->Unload(wxString::FromUTF8(Config["AppDir"]) + "/res/xrc/SliderDataImport.xrc"); +} + +int SliderDataImportDialog::ShowModal(const std::unordered_map>& sliderData) { + + std::unordered_set addedSliders; + for (auto& shapeSliders : sliderData) { + auto shapeName = project->TargetToShape(shapeSliders.first); + shapesCheckListBox->Append(shapeName, new ShapeSliderData(shapeName, shapeSliders.first, shapeSliders.second)); + + for (auto& sliderName : shapeSliders.second) { + if (addedSliders.find(sliderName.first) != addedSliders.end()) + continue; + + slidersCheckListBox->Append(sliderName.first); + addedSliders.emplace(sliderName.first); + } + } + + for (uint32_t i = 0; i < shapesCheckListBox->GetCount(); i++) + shapesCheckListBox->Check(i); + + for (uint32_t i = 0; i < slidersCheckListBox->GetCount(); i++) + slidersCheckListBox->Check(i); + + return wxDialog::ShowModal(); +} + +void SliderDataImportDialog::OnShapeSelectionChanged(wxCommandEvent& WXUNUSED(event)) { + slidersCheckListBox->Clear(); + std::unordered_set addedSliders; + + wxArrayInt checked; + shapesCheckListBox->GetCheckedItems(checked); + for (const auto& i : checked) { + const auto data = dynamic_cast(shapesCheckListBox->GetClientObject(i)); + for (auto& sliderName : data->sliderNames) { + if (addedSliders.find(sliderName.first) != addedSliders.end()) + continue; + + slidersCheckListBox->Append(sliderName.first); + addedSliders.emplace(sliderName.first); + } + } + + for (uint32_t i = 0; i < slidersCheckListBox->GetCount(); i++) + slidersCheckListBox->Check(i); +} + +void SliderDataImportDialog::OnImport(wxCommandEvent& WXUNUSED(event)) { + + options.mergeSliders = ConfigDialogUtil::SetBoolFromDialogCheckbox(outfitStudioConfig, *this, "chkImportMergeSliders"); + + wxArrayInt checked; + slidersCheckListBox->GetCheckedItems(checked); + std::unordered_set selectedSliders; + for (const auto& i : checked) { + const auto sliderDisplayName = slidersCheckListBox->GetString(i); + selectedSliders.emplace(sliderDisplayName); + } + + shapesCheckListBox->GetCheckedItems(checked); + for (const auto& i : checked) { + const auto data = dynamic_cast(shapesCheckListBox->GetClientObject(i)); + for (auto sliderNames : data->sliderNames) { + if (selectedSliders.find(sliderNames.first) == selectedSliders.end()) + continue; + + options.selectedShapesToSliders[data->targetShape].emplace(sliderNames.second, sliderNames.first); + } + } + + EndModal(wxID_OK); +} + +void SliderDataImportDialog::OnSliderListContext(wxMouseEvent& WXUNUSED(event)) { + wxMenu* menu = wxXmlResource::Get()->LoadMenu("sliderDataContext"); + if (menu) { + menu->Bind(wxEVT_MENU, &SliderDataImportDialog::OnSliderListContextSelect, this); + PopupMenu(menu); + delete menu; + } +} + +void SliderDataImportDialog::OnSliderListContextSelect(wxCommandEvent& event) { + if (event.GetId() == XRCID("sliderDataContextNone")) { + for (uint32_t i = 0; i < slidersCheckListBox->GetCount(); i++) + slidersCheckListBox->Check(i, false); + } + else if (event.GetId() == XRCID("sliderDataContextAll")) { + for (uint32_t i = 0; i < slidersCheckListBox->GetCount(); i++) + slidersCheckListBox->Check(i); + } + else if (event.GetId() == XRCID("sliderDataContextInvert")) { + for (uint32_t i = 0; i < slidersCheckListBox->GetCount(); i++) + slidersCheckListBox->Check(i, !slidersCheckListBox->IsChecked(i)); + } +} \ No newline at end of file diff --git a/src/program/SliderDataImportDialog.h b/src/program/SliderDataImportDialog.h new file mode 100644 index 00000000..e11d83b5 --- /dev/null +++ b/src/program/SliderDataImportDialog.h @@ -0,0 +1,58 @@ +/* +BodySlide and Outfit Studio +See the included LICENSE file +*/ + +#pragma once + +#include "ShapeProperties.h" + +#include +#include + +class OutfitProject; +class ConfigurationManager; + +class ShapeSliderData : public wxClientData { +public: + ShapeSliderData(const std::string& name, const std::string& originalName, const std::unordered_map& sliderNames) + : shapeName(originalName), targetShape(name), sliderNames(sliderNames) {} + std::string shapeName; + std::string targetShape; + std::unordered_map sliderNames; +}; + +class SliderDataImportOptions { +public: + SliderDataImportOptions() + : mergeSliders(false) {} + + std::unordered_map> selectedShapesToSliders; + bool mergeSliders; +}; + + +class SliderDataImportDialog : public wxDialog { +public: + SliderDataImportDialog(wxWindow* parent, OutfitProject* project, ConfigurationManager& outfitStudioConfig); + ~SliderDataImportDialog(); + + int ShowModal(const std::unordered_map>& sliderData); + void OnImport(wxCommandEvent& event); + + SliderDataImportOptions GetOptions() { return options; } + wxDECLARE_EVENT_TABLE(); + +private: + void OnShapeSelectionChanged(wxCommandEvent& WXUNUSED(event)); + void OnSliderListContext(wxMouseEvent& WXUNUSED(event)); + void OnSliderListContextSelect(wxCommandEvent& event); + + wxCheckListBox* shapesCheckListBox; + wxCheckListBox* slidersCheckListBox; + OutfitProject* project; + ConfigurationManager& outfitStudioConfig; + SliderDataImportOptions options; +}; + +