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 @@
+
+
+
+
+
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;
+};
+
+