Current state, all done except for file system selector.
Some checks failed
.NET Build / build (push) Has been cancelled

This commit is contained in:
Ottermandias 2025-12-28 00:06:32 +01:00
parent 97a14db4d5
commit cabcaadde3
41 changed files with 1749 additions and 1511 deletions

2
Luna

@ -1 +1 @@
Subproject commit e52d0dab9fd7f64d108125b79e387052fae2434f Subproject commit d81c788133b8b557febbad0bf74baee9588215eb

View file

@ -152,11 +152,28 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
var newCollection = Create(name, _collections.Count, duplicate); var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection); _collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success,
false);
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty)); _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty));
return true; return true;
} }
/// <summary> Rename a collection. </summary>
/// <param name="collection"> The collection to rename. </param>
/// <param name="newName"> The new name for the collection. </param>
/// <returns> True if a change has taken place. </returns>
public bool RenameCollection(ModCollection collection, string newName)
{
var oldName = collection.Identity.Name;
if (newName == oldName)
return false;
collection.Identity.Name = newName;
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
_communicator.CollectionRename.Invoke(new CollectionRename.Arguments(collection, oldName, newName));
return true;
}
/// <summary> /// <summary>
/// Remove the given collection if it exists and is neither the empty nor the default-named collection. /// Remove the given collection if it exists and is neither the empty nor the default-named collection.
/// </summary> /// </summary>
@ -355,7 +372,9 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, ISer
foreach (var collection in this) foreach (var collection in this)
{ {
if (collection.GetOwnSettings(arguments.Mod.Index)?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex) ?? false) if (collection.GetOwnSettings(arguments.Mod.Index)
?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex)
?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(arguments.Mod.Index, null); collection.Settings.SetTemporary(arguments.Mod.Index, null);
} }

View file

@ -39,8 +39,8 @@ public sealed class CollectionChange(Logger log)
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" /> /// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
ItemSwapTab = 0, ItemSwapTab = 0,
/// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" /> /// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionChange" />
CollectionSelector = 0, CollectionSelectorCache = 0,
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/> /// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
IndividualAssignmentUi = 0, IndividualAssignmentUi = 0,

View file

@ -0,0 +1,20 @@
using Luna;
using Penumbra.Collections;
namespace Penumbra.Communication;
public sealed class CollectionRename(Logger log)
: EventBase<CollectionRename.Arguments, CollectionRename.Priority>(nameof(CollectionRename), log)
{
public enum Priority
{
/// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionRename" />
CollectionSelectorCache = int.MinValue,
}
/// <summary> The arguments for a collection rename event. </summary>
/// <param name="Collection"> The renamed collection. </param>
/// <param name="OldName"> The old name of the collection. </param>
/// <param name="NewName"> The new name of the collection. </param>
public readonly record struct Arguments(ModCollection Collection, string OldName, string NewName);
}

View file

@ -26,25 +26,25 @@ public class EphemeralConfig : ISavable, IDisposable, IService
public float ModSelectorMinimumScale { get; set; } = 0.1f; public float ModSelectorMinimumScale { get; set; } = 0.1f;
public float ModSelectorMaximumScale { get; set; } = 0.5f; public float ModSelectorMaximumScale { get; set; } = 0.5f;
public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false; public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0; public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false; public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty; public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false; public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true; public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings; public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty; public string LastModPath { get; set; } = string.Empty;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = []; public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false; public bool IncognitoMode { get; set; } = false;
/// <summary> /// <summary>
/// Load the current configuration. /// Load the current configuration.

View file

@ -57,6 +57,7 @@ public class ModEditor(
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
Duplicates.Clear(); Duplicates.Clear();
MdlMaterialEditor.ScanModels(Mod!); MdlMaterialEditor.ScanModels(Mod!);
OptionLoaded?.Invoke();
}); });
} }
@ -81,13 +82,14 @@ public class ModEditor(
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
FileEditor.Clear(); FileEditor.Clear();
Duplicates.Clear(); Duplicates.Clear();
OptionLoaded?.Invoke();
}); });
} }
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary> /// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
private void LoadOption(int groupIdx, int dataIdx, bool message) private void LoadOption(int groupIdx, int dataIdx, bool message)
{ {
if (Mod != null && Mod.Groups.Count > groupIdx) if (Mod is not null && Mod.Groups.Count > groupIdx)
{ {
if (groupIdx == -1 && dataIdx == 0) if (groupIdx == -1 && dataIdx == 0)
{ {
@ -119,6 +121,8 @@ public class ModEditor(
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
} }
public event Action? OptionLoaded;
public void Clear() public void Clear()
{ {
Duplicates.Clear(); Duplicates.Clear();
@ -127,6 +131,7 @@ public class ModEditor(
MetaEditor.Clear(); MetaEditor.Clear();
Mod = null; Mod = null;
LoadOption(0, 0, false); LoadOption(0, 0, false);
OptionLoaded?.Invoke();
} }
public void Dispose() public void Dispose()
@ -146,7 +151,7 @@ public class ModEditor(
foreach (var subDir in baseDir.GetDirectories()) foreach (var subDir in baseDir.GetDirectories())
{ {
ClearEmptySubDirectories(subDir); ClearEmptySubDirectories(subDir);
if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0) if (subDir.GetFiles().Length is 0 && subDir.GetDirectories().Length is 0)
subDir.Delete(); subDir.Delete();
} }
} }

View file

@ -1,3 +1,4 @@
using ImSharp.Containers;
using Luna; using Luna;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -6,13 +7,13 @@ namespace Penumbra.Mods.Editor;
public class ModFileCollection : IDisposable public class ModFileCollection : IDisposable
{ {
private readonly List<FileRegistry> _available = []; private readonly ObservableList<FileRegistry> _available = [];
private readonly List<FileRegistry> _mtrl = []; private readonly ObservableList<FileRegistry> _mtrl = [];
private readonly List<FileRegistry> _mdl = []; private readonly ObservableList<FileRegistry> _mdl = [];
private readonly List<FileRegistry> _tex = []; private readonly ObservableList<FileRegistry> _tex = [];
private readonly List<FileRegistry> _shpk = []; private readonly ObservableList<FileRegistry> _shpk = [];
private readonly List<FileRegistry> _pbd = []; private readonly ObservableList<FileRegistry> _pbd = [];
private readonly List<FileRegistry> _atch = []; private readonly ObservableList<FileRegistry> _atch = [];
private readonly SortedSet<FullPath> _missing = []; private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = []; private readonly HashSet<Utf8GamePath> _usedPaths = [];
@ -23,25 +24,25 @@ public class ModFileCollection : IDisposable
public IReadOnlySet<Utf8GamePath> UsedPaths public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : []; => Ready ? _usedPaths : [];
public IReadOnlyList<FileRegistry> Available public IObservableList<FileRegistry> Available
=> Ready ? _available : []; => Ready ? _available : [];
public IReadOnlyList<FileRegistry> Mtrl public IObservableList<FileRegistry> Mtrl
=> Ready ? _mtrl : []; => Ready ? _mtrl : [];
public IReadOnlyList<FileRegistry> Mdl public IObservableList<FileRegistry> Mdl
=> Ready ? _mdl : []; => Ready ? _mdl : [];
public IReadOnlyList<FileRegistry> Tex public IObservableList<FileRegistry> Tex
=> Ready ? _tex : []; => Ready ? _tex : [];
public IReadOnlyList<FileRegistry> Shpk public IObservableList<FileRegistry> Shpk
=> Ready ? _shpk : []; => Ready ? _shpk : [];
public IReadOnlyList<FileRegistry> Pbd public IObservableList<FileRegistry> Pbd
=> Ready ? _pbd : []; => Ready ? _pbd : [];
public IReadOnlyList<FileRegistry> Atch public IObservableList<FileRegistry> Atch
=> Ready ? _atch : []; => Ready ? _atch : [];
public bool Ready { get; private set; } = true; public bool Ready { get; private set; } = true;
@ -123,24 +124,12 @@ public class ModFileCollection : IDisposable
_available.Add(registry); _available.Add(registry);
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant()) switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
{ {
case ".mtrl": case ".mtrl": _mtrl.Add(registry); break;
_mtrl.Add(registry); case ".mdl": _mdl.Add(registry); break;
break; case ".tex": _tex.Add(registry); break;
case ".mdl": case ".shpk": _shpk.Add(registry); break;
_mdl.Add(registry); case ".pbd": _pbd.Add(registry); break;
break; case ".atch": _atch.Add(registry); break;
case ".tex":
_tex.Add(registry);
break;
case ".shpk":
_shpk.Add(registry);
break;
case ".pbd":
_pbd.Add(registry);
break;
case ".atch":
_atch.Add(registry);
break;
} }
} }
} }

View file

@ -5,6 +5,9 @@ namespace Penumbra.Services;
public class CommunicatorService(ServiceManager services) : IService public class CommunicatorService(ServiceManager services) : IService
{ {
/// <inheritdoc cref="Communication.CollectionRename"/>
public readonly CollectionRename CollectionRename = services.GetService<CollectionRename>();
/// <inheritdoc cref="Communication.CollectionChange"/> /// <inheritdoc cref="Communication.CollectionChange"/>
public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>(); public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>();

View file

@ -124,7 +124,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
?? _config.Ephemeral.ResourceWatcherResourceCategories; ?? _config.Ephemeral.ResourceWatcherResourceCategories;
_config.Ephemeral.ResourceWatcherRecordTypes = _config.Ephemeral.ResourceWatcherRecordTypes =
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionPanelMode>() ?? _config.Ephemeral.CollectionPanel;
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>() _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
?? _config.Ephemeral.ChangedItemFilter; ?? _config.Ephemeral.ChangedItemFilter;

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using OtterGui.Widgets; using OtterGui.Widgets;
@ -8,121 +7,181 @@ using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Materials;
using FilterComboColors = Penumbra.UI.FilterComboColors;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.Services; namespace Penumbra.Services;
//public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text) public sealed record StainTemplate(StringPair Id, Vector4 Diffuse, Vector4 Specular, Vector4 Emissive, int Key, bool Found)
// where TDyePack : unmanaged, IDyePack
//{
// public override StringU8 DisplayString(in StmKeyType value)
// => new($"{value,4}");
//
// public override string FilterString(in StmKeyType value)
// => $"{value,4}";
//
// public override IEnumerable<StmKeyType> GetBaseItems()
// => throw new NotImplementedException();
//
// protected override bool DrawFilter(float width, FilterComboBaseCache<SimpleCacheItem<StmKeyType>> cache)
// {
// using var font = Im.Font.PushDefault();
// return base.DrawFilter(width, cache);
// }
//
// public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, Utf8StringHandler<HintStringHandlerBuffer> preview, Utf8StringHandler<TextStringHandlerBuffer> tooltip, ref int currentSelection, float previewWidth, float itemHeight,
// ComboFlags flags = ComboFlags.None)
// {
// using var font = Im.Font.PushMono();
// using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f))
// .PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
// var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
// var spaces = (int)(previewWidth / spaceSize) - 1;
// return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
// }
//
// protected override bool DrawSelectable(int globalIdx, bool selected)
// {
// var ret = base.DrawSelectable(globalIdx, selected);
// var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key;
// if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
// return ret;
//
// Im.Line.Same();
//
// var frame = new Vector2(Im.Style.TextHeight);
// Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame);
// Im.Line.Same();
// Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame);
// Im.Line.Same();
// Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
// return ret;
// }
//}
public class StainService : Luna.IService
{ {
public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) public Vector4 Diffuse { get; set; } = Diffuse;
: FilterComboCache<StmKeyType>(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log) public Vector4 Specular { get; set; } = Specular;
where TDyePack : unmanaged, IDyePack public Vector4 Emissive { get; set; } = Emissive;
public bool Found { get; set; } = Found;
}
public sealed class TemplateFilter : TextFilterBase<StainTemplate>
{
protected override string ToFilterString(in StainTemplate item, int globalIndex)
=> item.Id;
}
public sealed class StainTemplateCombo<TDyePack> : ImSharp.FilterComboBase<StainTemplate>
where TDyePack : unmanaged, IDyePack
{
private readonly StainService.StainCombo[] _stainCombos;
private readonly StmFile<TDyePack> _stmFile;
private int _currentDyeChannel;
private ushort _currentSelection;
public StainTemplateCombo(StainService.StainCombo[] stainCombos, StmFile<TDyePack> stmFile)
: base(new TemplateFilter())
{ {
// FIXME There might be a better way to handle that. PreviewAlignment = new Vector2(0.90f, 0.5f);
public int CurrentDyeChannel = 0; _stainCombos = stainCombos;
_stmFile = stmFile;
ComputeWidth = true;
}
protected override float GetFilterWidth() protected override FilterComboBaseCache<StainTemplate> CreateCache()
=> new Cache(this);
private sealed class Cache : FilterComboBaseCache<StainTemplate>
{
private readonly StainTemplateCombo<TDyePack> _parent;
private int _dyeChannel;
public Cache(StainTemplateCombo<TDyePack> parent)
: base(parent)
{ {
var baseSize = Im.Font.CalculateSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X; _parent = parent;
if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) foreach (var combo in _parent._stainCombos)
return baseSize; combo.SelectionChanged += OnSelectionChanged;
_dyeChannel = _parent._currentDyeChannel;
return baseSize + Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
} }
protected override string ToString(StmKeyType obj) public override void Update()
=> $"{obj,4}";
protected override void DrawFilter(int currentSelected, float width)
{ {
using var font = Im.Font.PushDefault(); base.Update();
base.DrawFilter(currentSelected, width); if (_dyeChannel != _parent._currentDyeChannel)
{
UpdateItems();
ComputeWidth();
_dyeChannel = _parent._currentDyeChannel;
}
} }
public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, private void OnSelectionChanged(Luna.FilterComboColors.Item obj)
ImGuiComboFlags flags = ImGuiComboFlags.None)
{ {
using var font = Im.Font.PushMono(); UpdateItems();
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f)) ComputeWidth();
.PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
var spaces = (int)(previewWidth / spaceSize) - 1;
return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
} }
protected override bool DrawSelectable(int globalIdx, bool selected) private void UpdateItems()
{ {
var ret = base.DrawSelectable(globalIdx, selected); foreach (var item in UnfilteredItems)
var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; {
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) var dye = _parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id;
return ret; if (dye > 0 && _parent._stmFile.TryGetValue(item.Key, dye, out var dyes))
{
item.Found = true;
item.Diffuse = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1);
item.Specular = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1);
item.Emissive = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1);
}
else
{
item.Found = false;
}
}
}
Im.Line.Same();
var frame = new Vector2(Im.Style.TextHeight); protected override void ComputeWidth()
Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); {
Im.Line.Same(); ComboWidth = Im.Font.Mono.CalculateTextSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X;
Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); if (_parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id is 0)
Im.Line.Same(); return;
Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
return ret; ComboWidth += Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
foreach (var combo in _parent._stainCombos)
combo.SelectionChanged -= OnSelectionChanged;
} }
} }
public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, ushort currentSelection, int currentDyeChannel,
Utf8StringHandler<TextStringHandlerBuffer> tooltip, out int newSelection, float previewWidth, float itemHeight,
ComboFlags flags = ComboFlags.None)
{
Flags = flags;
_currentDyeChannel = currentDyeChannel;
_currentSelection = currentSelection;
using var font = Im.Font.PushMono();
if (!base.Draw(label, $"{_currentSelection,4}", tooltip, previewWidth, out var selection))
{
newSelection = 0;
return false;
}
newSelection = selection.Key;
return true;
}
protected override IEnumerable<StainTemplate> GetItems()
{
var dye = _stainCombos[_currentDyeChannel].CurrentSelection.Id;
foreach (var key in _stmFile.Entries.Keys.Prepend(0))
{
if (dye > 0 && _stmFile.TryGetValue(key, dye, out var dyes))
yield return new StainTemplate(new StringPair($"{key,4}"),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1), key.Int, true);
else
yield return new StainTemplate(new StringPair($"{key,4}"), Vector4.Zero, Vector4.Zero, Vector4.Zero, key.Int, false);
}
}
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in StainTemplate item, int globalIndex, bool selected)
{
var ret = Im.Selectable(item.Id.Utf8, selected);
if (item.Found)
{
Im.Line.SameInner();
var frame = new Vector2(Im.Style.TextHeight);
Im.Color.Button("D"u8, item.Diffuse, 0, frame);
Im.Line.SameInner();
Im.Color.Button("S"u8, item.Specular, 0, frame);
Im.Line.SameInner();
Im.Color.Button("E"u8, item.Emissive, 0, frame);
}
return ret;
}
protected override bool IsSelected(StainTemplate item, int globalIndex)
=> item.Key == _currentSelection;
protected override bool DrawFilter(float width, FilterComboBaseCache<StainTemplate> cache)
{
using var font = Im.Font.PushDefault();
return base.DrawFilter(width, cache);
}
}
public class StainService : Luna.IService
{
public const int ChannelCount = 2; public const int ChannelCount = 2;
public readonly DictStain StainData; public readonly StainCombo StainCombo1;
public readonly FilterComboColors StainCombo1; public readonly StainCombo StainCombo2; // FIXME is there a better way to handle this?
public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this?
public readonly StmFile<LegacyDyePack> LegacyStmFile; public readonly StmFile<LegacyDyePack> LegacyStmFile;
public readonly StmFile<DyePack> GudStmFile; public readonly StmFile<DyePack> GudStmFile;
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo; public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
@ -130,9 +189,8 @@ public class StainService : Luna.IService
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
{ {
StainData = stainData; StainCombo1 = new StainCombo(stainData);
StainCombo1 = CreateStainCombo(); StainCombo2 = new StainCombo(stainData);
StainCombo2 = CreateStainCombo();
if (characterUtility.Address == null) if (characterUtility.Address == null)
{ {
@ -146,14 +204,14 @@ public class StainService : Luna.IService
} }
FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; StainCombo[] stainCombos = [StainCombo1, StainCombo2];
LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile); LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile);
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile); GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
} }
/// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary> /// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary>
public FilterComboColors GetStainCombo(int channel) public StainCombo GetStainCombo(int channel)
=> channel switch => channel switch
{ {
0 => StainCombo1, 0 => StainCombo1,
@ -180,8 +238,9 @@ public class StainService : Luna.IService
return new StmFile<TDyePack>(dataManager); return new StmFile<TDyePack>(dataManager);
} }
private FilterComboColors CreateStainCombo() public sealed class StainCombo(DictStain stainData) : Luna.FilterComboColors
=> new(140, MouseWheelType.None, {
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(), protected override IEnumerable<Item> GetItems()
Penumbra.Log); => stainData.Value.Select(t => new Item(new StringPair(t.Value.Name), t.Value.Dye, t.Key, t.Value.Gloss)).Prepend(None);
}
} }

View file

@ -2,8 +2,6 @@ using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -11,7 +9,6 @@ using Penumbra.Mods.Editor;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -24,7 +21,7 @@ public class FileEditor<T>(
FileDialogService fileDialog, FileDialogService fileDialog,
string tabName, string tabName,
string fileType, string fileType,
Func<IReadOnlyList<FileRegistry>> getFiles, Func<IEnumerable<FileRegistry>> getFiles,
Func<T, bool, bool> drawEdit, Func<T, bool, bool> drawEdit,
Func<string> getInitialPath, Func<string> getInitialPath,
Func<byte[], string, bool, T?> parseFile) Func<byte[], string, bool, T?> parseFile)
@ -74,10 +71,15 @@ public class FileEditor<T>(
_defaultFile = null; _defaultFile = null;
} }
private FileRegistry? _currentPath; private FileRegistry? CurrentPath
private T? _currentFile; {
private Exception? _currentException; get => _combo.Selected;
private bool _changed; set => _combo.Selected = value;
}
private T? _currentFile;
private Exception? _currentException;
private bool _changed;
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty; private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
private bool _inInput; private bool _inInput;
@ -163,7 +165,7 @@ public class FileEditor<T>(
public void Reset() public void Reset()
{ {
_currentException = null; _currentException = null;
_currentPath = null; CurrentPath = null;
(_currentFile as IDisposable)?.Dispose(); (_currentFile as IDisposable)?.Dispose();
_currentFile = null; _currentFile = null;
_changed = false; _changed = false;
@ -171,26 +173,32 @@ public class FileEditor<T>(
private void DrawFileSelectCombo() private void DrawFileSelectCombo()
{ {
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty, if (CurrentPath is not null)
Im.ContentRegion.Available.X, Im.Style.TextHeight) {
&& _combo.CurrentSelection != null) if (_combo.Draw("##select"u8, CurrentPath.RelPath.Path.Span, StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
UpdateCurrentFile(_combo.CurrentSelection); UpdateCurrentFile(newSelection);
}
else
{
if (_combo.Draw("##select"u8, $"Select {fileType} File...", StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
UpdateCurrentFile(newSelection);
}
} }
private void UpdateCurrentFile(FileRegistry path) private void UpdateCurrentFile(FileRegistry path)
{ {
if (ReferenceEquals(_currentPath, path)) if (ReferenceEquals(CurrentPath, path))
return; return;
_changed = false; _changed = false;
_currentPath = path; CurrentPath = path;
_currentException = null; _currentException = null;
try try
{ {
var bytes = File.ReadAllBytes(_currentPath.File.FullName); var bytes = File.ReadAllBytes(CurrentPath.File.FullName);
(_currentFile as IDisposable)?.Dispose(); (_currentFile as IDisposable)?.Dispose();
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
_currentFile = parseFile(bytes, _currentPath.File.FullName, true); _currentFile = parseFile(bytes, CurrentPath.File.FullName, true);
} }
catch (Exception e) catch (Exception e)
{ {
@ -210,9 +218,9 @@ public class FileEditor<T>(
public void SaveFile() public void SaveFile()
{ {
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); compactor.WriteAllBytes(CurrentPath!.File.FullName, _currentFile!.Write());
if (owner.Mod is not null) if (owner.Mod is not null)
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath)); communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, CurrentPath));
_changed = false; _changed = false;
} }
@ -221,8 +229,8 @@ public class FileEditor<T>(
if (ImEx.Button("Reset Changes"u8, Vector2.Zero, if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
$"Reset all changes made to the {fileType} file.", !_changed)) $"Reset all changes made to the {fileType} file.", !_changed))
{ {
var tmp = _currentPath; var tmp = CurrentPath;
_currentPath = null; CurrentPath = null;
UpdateCurrentFile(tmp!); UpdateCurrentFile(tmp!);
} }
} }
@ -233,7 +241,7 @@ public class FileEditor<T>(
if (!child) if (!child)
return; return;
if (_currentPath is not null) if (CurrentPath is not null)
{ {
if (_currentFile is null) if (_currentFile is null)
{ {
@ -253,7 +261,7 @@ public class FileEditor<T>(
if (!_inInput && _defaultPath.Length > 0) if (!_inInput && _defaultPath.Length > 0)
{ {
if (_currentPath is not null) if (CurrentPath is not null)
{ {
Im.Line.New(); Im.Line.New();
Im.Line.New(); Im.Line.New();
@ -278,47 +286,70 @@ public class FileEditor<T>(
} }
} }
private class Combo(Func<IReadOnlyList<FileRegistry>> generator) private sealed class Combo : FilterComboBase<FileRegistry>
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
{ {
protected override bool DrawSelectable(int globalIdx, bool selected) private sealed class FileFilter : RegexFilterBase<FileRegistry>
{
// TODO: Avoid ToString.
public override bool WouldBeVisible(in FileRegistry item, int globalIndex)
=> WouldBeVisible(item.File.FullName) || item.SubModUsage.Any(f => WouldBeVisible(f.Item2.ToString()));
/// <summary> Unused. </summary>
protected override string ToFilterString(in FileRegistry item, int globalIndex)
=> string.Empty;
}
private readonly Func<IEnumerable<FileRegistry>> _getFiles;
public FileRegistry? Selected;
public Combo(Func<IEnumerable<FileRegistry>> getFiles)
{
_getFiles = getFiles;
Filter = new FileFilter();
}
protected override IEnumerable<FileRegistry> GetItems()
=> _getFiles();
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in FileRegistry item, int globalIndex, bool selected)
{ {
var file = Items[globalIdx];
bool ret; bool ret;
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), item.IsOnPlayer))
{ {
ret = Im.Selectable(file.RelPath.ToString(), selected); ret = Im.Selectable(item.RelPath.Path.Span, selected);
} }
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding);
using var tt = Im.Tooltip.Begin();
Im.Text("All Game Paths"u8); Im.Text("All Game Paths"u8);
Im.Separator(); Im.Separator();
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit); using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
if (t) if (t)
{ foreach (var (option, gamePath) in item.SubModUsage)
foreach (var (option, gamePath) in file.SubModUsage)
{ {
t.DrawColumn(gamePath.Path.Span); t.DrawColumn(gamePath.Path.Span);
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
t.DrawColumn(option.GetFullName()); t.DrawColumn(option.GetFullName());
} }
}
} }
if (file.SubModUsage.Count > 0) if (item.SubModUsage.Count > 0)
{ {
Im.Line.Same(); Im.Line.Same();
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}"); ImEx.TextRightAligned($"{item.SubModUsage[0].Item2.Path}");
} }
return ret; return ret;
} }
protected override bool IsVisible(int globalIndex, LowerString filter) protected override bool IsSelected(FileRegistry item, int globalIndex)
=> filter.IsContained(Items[globalIndex].File.FullName) => item.Equals(Selected);
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
} }
} }

View file

@ -0,0 +1,94 @@
using ImSharp;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
namespace Penumbra.UI.AdvancedWindow;
public sealed class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type)
: FilterComboBase<ItemSelector.CacheItem>(new ItemFilter())
{
public EquipItem CurrentSelection = new(string.Empty, default, default, default, default, default, FullEquipType.Unknown, default, default,
default);
public sealed record CacheItem(EquipItem Item, StringPair Name, Vector4 Color, bool InCurrentMod, StringU8[] CollectionMods)
{
public Vector4 Color { get; set; } = Color;
public CacheItem(EquipItem item, Mod currentMod, ModCollection currentCollection)
: this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], currentMod.ChangedItems.Any(c => c.Key == item.Name),
ConvertCollection(item, currentCollection))
{
if (InCurrentMod)
Color = ColorId.ResTreeLocalPlayer.Value().ToVector();
else if (CollectionMods.Length > 0)
Color = ColorId.ResTreeNonNetworked.Value().ToVector();
}
public CacheItem(EquipItem item, ModCollection currentCollection)
: this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], false, ConvertCollection(item, currentCollection))
{
if (CollectionMods.Length > 0)
Color = ColorId.ResTreeNonNetworked.Value().ToVector();
}
private static StringU8[] ConvertCollection(in EquipItem item, ModCollection collection)
{
if (!collection.ChangedItems.TryGetValue(item.Name, out var mods))
return [];
var ret = new StringU8[mods.Item1.Count];
for (var i = 0; i < mods.Item1.Count; ++i)
ret[i] = new StringU8(mods.Item1[i].Name);
return ret;
}
}
private sealed class ItemFilter : PartwiseFilterBase<CacheItem>
{
protected override string ToFilterString(in CacheItem item, int globalIndex)
=> item.Name.Utf16;
}
protected override IEnumerable<CacheItem> GetItems()
{
var list = data.ByType[type];
if (selector?.Selected is { } currentMod && currentMod.ChangedItems.Values.Any(c => c is IdentifiedItem i && i.Item.Type == type))
return list.Select(item => new CacheItem(item, currentMod, collections.Current)).OrderByDescending(i => i.InCurrentMod)
.ThenByDescending(i => i.CollectionMods.Length);
if (selector is null)
return list.Select(item => new CacheItem(item, collections.Current)).OrderBy(i => i.CollectionMods.Length);
return list.Select(item => new CacheItem(item, collections.Current)).OrderByDescending(i => i.CollectionMods.Length);
}
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in CacheItem item, int globalIndex, bool selected)
{
using var color = item.Color.W > 0 ? ImGuiColor.Text.Push(item.Color) : null;
var ret = Im.Selectable(item.Name.Utf8, selected);
if (item.CollectionMods.Length > 0 && Im.Item.Hovered())
{
using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding);
using var tt = Im.Tooltip.Begin();
foreach (var mod in item.CollectionMods)
Im.Text(mod);
}
if (ret)
CurrentSelection = item.Item;
return ret;
}
protected override bool IsSelected(CacheItem item, int globalIndex)
=> item.Item.Equals(CurrentSelection);
}

View file

@ -2,7 +2,6 @@ using Dalamud.Interface.ImGuiNotification;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Luna.Generators; using Luna.Generators;
using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
@ -12,7 +11,6 @@ using Penumbra.GameData.Structs;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
@ -22,8 +20,6 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using ITab = OtterGui.Widgets.ITab;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -152,44 +148,6 @@ public class ItemSwapTab : IDisposable, ITab
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
} }
private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type)
: FilterComboCache<(EquipItem Item, bool InMod, SingleArray<IMod> InCollection)>(() =>
{
var list = data.ByType[type];
var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)
? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name),
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count)
: selector is null
? list.Select(i => (i, false,
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderBy(p => p.Item3.Count)
: list.Select(i => (i, false,
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderByDescending(p => p.Item3.Count);
return enumerable.ToList();
}, MouseWheelType.None, Penumbra.Log)
{
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var (_, inMod, inCollection) = Items[globalIdx];
using var color = inMod
? ImGuiColor.Text.Push(ColorId.ResTreeLocalPlayer.Value())
: inCollection.Count > 0
? ImGuiColor.Text.Push(ColorId.ResTreeNonNetworked.Value())
: null;
var ret = base.DrawSelectable(globalIdx, selected);
if (inCollection.Count > 0)
Im.Tooltip.OnHover(string.Join('\n', inCollection.Select(m => m.Name)));
return ret;
}
protected override string ToString((EquipItem Item, bool InMod, SingleArray<IMod> InCollection) obj)
=> obj.Item.Name;
}
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors; private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors;
private readonly ItemSwapContainer _swapData; private readonly ItemSwapContainer _swapData;
@ -241,17 +199,17 @@ public class ItemSwapTab : IDisposable, ITab
case SwapType.Ring: case SwapType.Ring:
case SwapType.Glasses: case SwapType.Glasses:
var values = _selectors[_lastTab]; var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown if (values.Source.CurrentSelection.Type is not FullEquipType.Unknown
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) && values.Target.CurrentSelection.Type is not FullEquipType.Unknown)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
break; break;
case SwapType.BetweenSlots: case SwapType.BetweenSlots:
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid)
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection, ToEquipSlot(_slotFrom),
selectorFrom.CurrentSelection.Item, selectorFrom.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
@ -315,10 +273,10 @@ public class ItemSwapTab : IDisposable, ITab
$"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}";
case SwapType.BetweenSlots: case SwapType.BetweenSlots:
return return
$"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
default: default:
return return
$"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
} }
} }
@ -543,9 +501,7 @@ public class ItemSwapTab : IDisposable, ITab
} }
table.NextColumn(); table.NextColumn();
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name, string.Empty, _dirty |= selector.Draw("##itemSource"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
(article1, _, selector) = GetAccessorySelector(_slotTo, false); (article1, _, selector) = GetAccessorySelector(_slotTo, false);
table.DrawFrameColumn($"and put {article2} on {article1}"); table.DrawFrameColumn($"and put {article2} on {article1}");
@ -566,8 +522,7 @@ public class ItemSwapTab : IDisposable, ITab
} }
table.NextColumn(); table.NextColumn();
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * Im.Style.GlobalScale, _dirty |= selector.Draw("##itemTarget"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
Im.Style.TextHeightWithSpacing);
if (_affectedItems is not { Count: > 1 }) if (_affectedItems is not { Count: > 1 })
return; return;
@ -576,7 +531,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name))) foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)))
Im.Text(item.Name); Im.Text(item.Name);
} }
} }
@ -610,8 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
return; return;
table.DrawFrameColumn(text1); table.DrawFrameColumn(text1);
table.NextColumn(); table.NextColumn();
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty, _dirty |= sourceSelector.Draw("##itemSource"u8, sourceSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing);
if (type is SwapType.Ring) if (type is SwapType.Ring)
{ {
@ -621,9 +575,7 @@ public class ItemSwapTab : IDisposable, ITab
table.DrawFrameColumn(text2); table.DrawFrameColumn(text2);
table.NextColumn(); table.NextColumn();
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty, _dirty |= targetSelector.Draw("##itemTarget"u8, targetSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
if (type is SwapType.Ring) if (type is SwapType.Ring)
{ {
Im.Line.Same(); Im.Line.Same();
@ -638,7 +590,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name))) foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)))
Im.Text(item.Name); Im.Text(item.Name);
} }
} }

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
@ -87,8 +86,8 @@ public partial class MtrlTab
var rowBIdx = rowAIdx | 1; var rowBIdx = rowAIdx | 1;
var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default;
var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default;
var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Id;
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Id;
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
using (var columns = Im.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
@ -599,11 +598,10 @@ public partial class MtrlTab
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
Im.Line.Same(subColWidth); Im.Line.Same(subColWidth);
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
_stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.GudTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
{ {
dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort; dye.Template = (ushort) newSelection;
ret = true; ret = true;
} }
@ -613,8 +611,8 @@ public partial class MtrlTab
using var dis = Im.Disabled(!dyePack.HasValue); using var dis = Im.Disabled(!dyePack.HasValue);
if (Im.Button("Apply Preview Dye"u8)) if (Im.Button("Apply Preview Dye"u8))
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
], rowIdx); ], rowIdx);
return ret; return ret;

View file

@ -83,8 +83,8 @@ public partial class MtrlTab
private bool DrawPreviewDye(bool disabled) private bool DrawPreviewDye(bool disabled)
{ {
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; var (name1, _, dyeId1, _) = _stainService.StainCombo1.CurrentSelection;
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; var (name2, _, dyeId2, _) = _stainService.StainCombo2.CurrentSelection;
var tt = dyeId1 is 0 && dyeId2 is 0 var tt = dyeId1 is 0 && dyeId2 is 0
? "Select a preview dye first."u8 ? "Select a preview dye first."u8
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
@ -103,12 +103,10 @@ public partial class MtrlTab
} }
Im.Line.Same(); Im.Line.Same();
var label = dyeId1 is 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; if (_stainService.StainCombo1.Draw(dyeId1 is 0 ? "Preview Dye 1###pd1" : $"{name1} (Preview 1)###pd1"))
if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1))
UpdateColorTablePreview(); UpdateColorTablePreview();
Im.Line.Same(); Im.Line.Same();
label = dyeId2 is 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; if (_stainService.StainCombo2.Draw(dyeId2 is 0 ? "Preview Dye 2###pd2" : $"{name2} (Preview 2)###pd2"))
if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2))
UpdateColorTablePreview(); UpdateColorTablePreview();
return false; return false;
} }

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
@ -177,13 +176,13 @@ public partial class MtrlTab
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
m => table[rowIdx].TileTransform = m); m => table[rowIdx].TileTransform = m);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Table.NextColumn(); Im.Table.NextColumn();
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, 0, StringU8.Empty, out var newSelection,
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton)) intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
{ {
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; dyeTable[rowIdx].Template = (ushort)newSelection;
ret = true; ret = true;
} }
@ -294,11 +293,10 @@ public partial class MtrlTab
ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f,
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
Im.Line.SameInner(); Im.Line.SameInner();
_stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
{ {
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; dyeTable[rowIdx].Template = (ushort)newSelection;
ret = true; ret = true;
} }
@ -313,8 +311,8 @@ public partial class MtrlTab
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize)
{ {
var stain = _stainService.StainCombo1.CurrentSelection.Key; var stain = _stainService.StainCombo1.CurrentSelection.Id;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false; return false;
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
@ -331,8 +329,8 @@ public partial class MtrlTab
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize)
{ {
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Id;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false; return false;
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
@ -341,8 +339,8 @@ public partial class MtrlTab
ret = ret ret = ret
&& Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
], rowIdx); ], rowIdx);
Im.Line.Same(); Im.Line.Same();

View file

@ -226,7 +226,7 @@ public partial class MtrlTab
}; };
if (dyeRow.Channel < StainService.ChannelCount) if (dyeRow.Channel < StainService.ChannelCount)
{ {
StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Id;
if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes))
row.ApplyDye(dyeRow, legacyDyes); row.ApplyDye(dyeRow, legacyDyes);
if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes))
@ -260,8 +260,8 @@ public partial class MtrlTab
{ {
ReadOnlySpan<StainId> stainIds = ReadOnlySpan<StainId> stainIds =
[ [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
]; ];
rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows);
rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows);

View file

@ -41,8 +41,12 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
var height = ColumnHeight; var height = ColumnHeight;
using var clipper = new Im.ListClipper(Count, height); using var clipper = new Im.ListClipper(Count, height);
foreach (var (identifier, value) in clipper.Iterate(Enumerate())) foreach (var (index, (identifier, value)) in clipper.Iterate(Enumerate().Index()))
{
id.Push(index);
DrawEntry(identifier, value); DrawEntry(identifier, value);
id.Pop();
}
} }
public abstract ReadOnlySpan<byte> Label { get; } public abstract ReadOnlySpan<byte> Label { get; }

View file

@ -1,5 +1,4 @@
using ImSharp; using ImSharp;
using OtterGui;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -61,17 +60,19 @@ public partial class ModEditWindow
} }
} }
private sealed class BoneCache(PbdData pbdData) : BasicFilterCache<string>(pbdData.BoneFilter)
{
protected override IEnumerable<string> GetItems()
=> pbdData.SelectedDeformer is null || pbdData.SelectedDeformer.IsEmpty ? [] : pbdData.SelectedDeformer.DeformMatrices.Keys;
}
private void DrawBoneSelector() private void DrawBoneSelector()
{ {
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Get((int)_pbdData.SelectedRaceCode), () => new BoneCache(_pbdData));
using var group = Im.Group(); using var group = Im.Group();
var width = 200 * Im.Style.GlobalScale; var width = 200 * Im.Style.GlobalScale;
using (ImStyleSingle.FrameRounding.Push(0) _pbdData.BoneFilter.DrawFilter("Filter..."u8, new Vector2(width, Im.Style.FrameHeight));
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)) Im.Cursor.Y -= Im.Style.ItemSpacing.Y;
{
Im.Item.SetNextWidth(width);
Im.Input.Text("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8);
}
using var child = Im.Child.Begin("Bone"u8, using var child = Im.Child.Begin("Bone"u8,
new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true); new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true);
if (!child) if (!child)
@ -80,23 +81,14 @@ public partial class ModEditWindow
if (_pbdData.SelectedDeformer is null) if (_pbdData.SelectedDeformer is null)
return; return;
if (_pbdData.SelectedDeformer.IsEmpty) if (cache.AllItems.Count is 0)
{
Im.Text("<Empty>"u8); Im.Text("<Empty>"u8);
}
else else
{ foreach (var item in cache)
var height = Im.Style.TextHeightWithSpacing; {
var skips = ImGuiClip.GetNecessarySkips(height); if (Im.Selectable(item, item == _pbdData.SelectedBone))
var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, _pbdData.SelectedBone = item;
b => b.Contains(_pbdData.BoneFilter), bone }
=>
{
if (Im.Selectable(bone, bone == _pbdData.SelectedBone))
_pbdData.SelectedBone = bone;
});
ImGuiClip.DrawEndDummy(remainder, height);
}
} }
private bool DrawBoneData(PbdTab tab, bool disabled) private bool DrawBoneData(PbdTab tab, bool disabled)
@ -324,8 +316,8 @@ public partial class ModEditWindow
public GenderRace SelectedRaceCode = GenderRace.Unknown; public GenderRace SelectedRaceCode = GenderRace.Unknown;
public RacialDeformer? SelectedDeformer; public RacialDeformer? SelectedDeformer;
public string? SelectedBone; public string? SelectedBone;
public TextFilter BoneFilter = new();
public string NewBoneName = string.Empty; public string NewBoneName = string.Empty;
public string BoneFilter = string.Empty;
public string RaceCodeFilter = string.Empty; public string RaceCodeFilter = string.Empty;
public TransformMatrix? CopiedMatrix; public TransformMatrix? CopiedMatrix;

View file

@ -1,7 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -20,10 +19,7 @@ public partial class ModEditWindow
private int _pathIdx = -1; private int _pathIdx = -1;
private int _folderSkip; private int _folderSkip;
private bool _overviewMode; private bool _overviewMode;
private readonly OverviewTable _overviewTable;
private string _fileOverviewFilter1 = string.Empty;
private string _fileOverviewFilter2 = string.Empty;
private string _fileOverviewFilter3 = string.Empty;
private bool CheckFilter(FileRegistry registry) private bool CheckFilter(FileRegistry registry)
=> _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase); => _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase);
@ -40,9 +36,7 @@ public partial class ModEditWindow
DrawOptionSelectHeader(); DrawOptionSelectHeader();
DrawButtonHeader(); DrawButtonHeader();
if (_overviewMode) if (!_overviewMode)
DrawFileManagementOverview();
else
DrawFileManagementNormal(); DrawFileManagementNormal();
using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true); using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true);
@ -50,65 +44,11 @@ public partial class ModEditWindow
return; return;
if (_overviewMode) if (_overviewMode)
DrawFilesOverviewMode(); _overviewTable.Draw();
else else
DrawFilesNormalMode(); DrawFilesNormalMode();
} }
private void DrawFilesOverviewMode()
{
var height = Im.Style.TextHeightWithSpacing + 2 * Im.Style.CellPadding.Y;
var skips = ImGuiClip.GetNecessarySkips(height);
using var table = Im.Table.Begin("##table"u8, 3, TableFlags.RowBackground | TableFlags.BordersInnerVertical, Im.ContentRegion.Available);
if (!table)
return;
var width = Im.ContentRegion.Available.X / 8;
table.SetupColumn("##file"u8, TableColumnFlags.WidthFixed, width * 3);
table.SetupColumn("##path"u8, TableColumnFlags.WidthFixed, width * 3 + Im.Style.FrameBorderThickness);
table.SetupColumn("##option"u8, TableColumnFlags.WidthFixed, width * 2);
var idx = 0;
var files = _editor.Files.Available.SelectMany(f =>
{
var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(),
_editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u));
});
void DrawLine((string, string, string, uint) data)
{
using var id = Im.Id.Push(idx++);
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item1);
Im.Table.NextColumn();
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item2);
Im.Table.NextColumn();
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item3);
}
bool Filter((string, string, string, uint) data)
=> data.Item1.Contains(_fileOverviewFilter1, StringComparison.OrdinalIgnoreCase)
&& data.Item2.Contains(_fileOverviewFilter2, StringComparison.OrdinalIgnoreCase)
&& data.Item3.Contains(_fileOverviewFilter3, StringComparison.OrdinalIgnoreCase);
var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine);
ImGuiClip.DrawEndDummy(end, height);
}
private void DrawFilesNormalMode() private void DrawFilesNormalMode()
{ {
@ -154,7 +94,7 @@ public partial class ModEditWindow
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
using var c = ImGuiColor.Text.PushDefault(); using var c = ImGuiColor.Text.PushDefault();
Im.Text(StringU8.Join((byte) '\n', text)); Im.Text(StringU8.Join((byte)'\n', text));
} }
@ -255,7 +195,7 @@ public partial class ModEditWindow
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
var pos = Im.Cursor.X - Im.Style.FrameHeight; var pos = Im.Cursor.X - Im.Style.FrameHeight;
Im.Item.SetNextWidth(-1); Im.Item.SetNextWidth(-1);
if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength:Utf8GamePath.MaxGamePathLength)) if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength: Utf8GamePath.MaxGamePathLength))
{ {
_fileIdx = i; _fileIdx = i;
_pathIdx = j; _pathIdx = j;
@ -341,7 +281,8 @@ public partial class ModEditWindow
if (Im.Button("Add Paths"u8)) if (Im.Button("Add Paths"u8))
_editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip); _editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip);
Im.Tooltip.OnHover("Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8); Im.Tooltip.OnHover(
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8);
Im.Line.Same(); Im.Line.Same();
@ -365,7 +306,7 @@ public partial class ModEditWindow
Im.Line.Same(); Im.Line.Same();
var changes = _editor.FileEditor.Changes; var changes = _editor.FileEditor.Changes;
var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8; var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8;
if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes)) if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes))
{ {
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
@ -412,22 +353,4 @@ public partial class ModEditWindow
ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected"); ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
} }
private void DrawFileManagementOverview()
{
using var style = ImStyleSingle.FrameRounding.Push(0)
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)
.Push(ImStyleSingle.FrameBorderThickness, Im.Style.ChildBorderThickness);
var width = Im.ContentRegion.Available.X / 8;
Im.Item.SetNextWidth(width * 3);
Im.Input.Text("##fileFilter"u8, ref _fileOverviewFilter1, "Filter file..."u8);
Im.Line.Same();
Im.Item.SetNextWidth(width * 3);
Im.Input.Text("##pathFilter"u8, ref _fileOverviewFilter2, "Filter path..."u8);
Im.Line.Same();
Im.Item.SetNextWidth(width * 2);
Im.Input.Text("##optionFilter"u8, ref _fileOverviewFilter3, "Filter option..."u8);
}
} }

View file

@ -107,7 +107,7 @@ public partial class ModEditWindow
private void DrawEditHeader(MetaManipulationType type) private void DrawEditHeader(MetaManipulationType type)
{ {
var drawer = _metaDrawers.Get(type); var drawer = _metaDrawers.Get(type);
if (drawer == null) if (drawer is null)
return; return;
var oldPos = Im.Cursor.Y; var oldPos = Im.Cursor.Y;

View file

@ -621,6 +621,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
_fileDialog = fileDialog; _fileDialog = fileDialog;
_framework = framework; _framework = framework;
_metaDrawers = metaDrawers; _metaDrawers = metaDrawers;
_overviewTable = new OverviewTable(_editor);
_optionSelect = new OptionSelectCombo(editor, this); _optionSelect = new OptionSelectCombo(editor, this);
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", _materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,

View file

@ -0,0 +1,141 @@
using ImSharp;
using ImSharp.Containers;
using ImSharp.Table;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.UI.AdvancedWindow;
public sealed class OverviewTable(ModEditor parent)
: DefaultTable<OverviewTable.OverviewFile>(new StringU8("##overview"u8),
new FileColumn
{
Label = new StringU8("File"u8),
Flags = TableColumnFlags.WidthStretch,
},
new PathColumn
{
Label = new StringU8("Path"u8),
Flags = TableColumnFlags.WidthStretch,
},
new OptionColumn
{
Label = new StringU8("Option"u8),
Flags = TableColumnFlags.WidthStretch,
})
{
public sealed record OverviewFile(
StringPair File,
StringPair Path,
StringPair OptionName,
IModDataContainer? Option,
ColorParameter Color)
{
public ColorParameter Color { get; set; } = Color;
public OverviewFile(FileRegistry file)
: this(new StringPair(file.RelPath.Path.Span), new StringPair("Unused", new StringU8("Unused"u8)), StringPair.Empty, null,
0x40000080)
{ }
public OverviewFile(FileRegistry file, IModDataContainer option, Utf8GamePath gamePath, bool tint)
: this(new StringPair(file.RelPath.Path.Span), new StringPair(gamePath.Path.Span), new StringPair(option.GetFullName()),
option, tint ? 0x40008000 : ColorParameter.Default)
{ }
}
private sealed class FileColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.File;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.File;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.File.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 3 / 8f;
}
private sealed class PathColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.Path;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.Path;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.Path.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 3 / 8f;
}
private sealed class OptionColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.OptionName;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.OptionName;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.OptionName.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 2 / 8f;
}
public override IEnumerable<OverviewFile> GetItems()
=> parent.Files.Available.SelectMany(f =>
{
return f.SubModUsage.Count is 0
? [new OverviewFile(f)]
: f.SubModUsage.Select(s => new OverviewFile(f, s.Item1, s.Item2, parent.Option! == s.Item1 && parent.Mod!.HasOptions));
});
protected override TableCache<OverviewFile> CreateCache()
=> new Cache(this, parent);
private sealed class Cache : TableCache<OverviewFile>
{
private readonly ModEditor _editor;
public Cache(OverviewTable table, ModEditor editor)
: base(table)
{
_editor = editor;
_editor.Files.Available.OnChange += OnChange;
_editor.OptionLoaded += OnOptionLoaded;
}
private void OnOptionLoaded()
{
foreach (var item in UnfilteredItems.Where(i => i.Option is not null))
item.Color = _editor.Option == item.Option && _editor.Mod!.HasOptions ? 0x40008000 : ColorParameter.Default;
}
private void OnChange(in ObservableList<FileRegistry>.ChangeArguments args)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_editor.Files.Available.OnChange -= OnChange;
_editor.OptionLoaded -= OnOptionLoaded;
}
}
}

View file

@ -46,7 +46,8 @@ public class CollectionSelectHeader(
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_activeCollections.CurrentCollectionInUse) if (!_activeCollections.CurrentCollectionInUse)
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
Colors.PressEnterWarningBg);
} }
private void DrawTemporaryCheckbox() private void DrawTemporaryCheckbox()
@ -185,6 +186,7 @@ public class CollectionSelectHeader(
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_activeCollections.CurrentCollectionInUse) if (!_activeCollections.CurrentCollectionInUse)
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
Colors.PressEnterWarningBg);
} }
} }

View file

@ -24,7 +24,7 @@ public class IncognitoService(TutorialService tutorial, Configuration config) :
} }
if (!hold) if (!hold)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); Im.Tooltip.OnHover($"\nHold {config.IncognitoModifier} while clicking to toggle.", HoveredFlags.AllowWhenDisabled, true);
tutorial.OpenTutorial(BasicTutorialSteps.Incognito); tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
} }

View file

@ -0,0 +1,13 @@
using ImSharp;
using Luna;
namespace Penumbra.UI.CollectionTab;
public sealed class CollectionFilter : TextFilterBase<CollectionSelector.Entry>, IUiService
{
public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex)
=> base.WouldBeVisible(in item, globalIndex) || WouldBeVisible(item.AnonymousName.Utf16);
protected override string ToFilterString(in CollectionSelector.Entry item, int globalIndex)
=> item.Collection.Identity.Name;
}

View file

@ -1,4 +1,3 @@
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
@ -14,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
@ -26,8 +26,9 @@ public sealed class CollectionPanel(
ITargetManager targets, ITargetManager targets,
ModStorage mods, ModStorage mods,
SaveService saveService, SaveService saveService,
IncognitoService incognito) IncognitoService incognito,
: IDisposable Configuration config)
: IDisposable, IPanel
{ {
private readonly CollectionStorage _collections = manager.Storage; private readonly CollectionStorage _collections = manager.Storage;
private readonly ActiveCollections _active = manager.Active; private readonly ActiveCollections _active = manager.Active;
@ -223,13 +224,8 @@ public sealed class CollectionPanel(
{ {
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f)); using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
Im.Item.SetNextWidth(width); Im.Item.SetNextWidth(width);
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName) if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName))
&& newName != collection.Identity.Name) _collections.RenameCollection(collection, newName);
{
collection.Identity.Name = newName;
saveService.QueueSave(new ModCollectionSave(mods, collection));
selector.RestoreCollections();
}
} }
if (_collections.DefaultNamed == collection) if (_collections.DefaultNamed == collection)
@ -770,4 +766,18 @@ public sealed class CollectionPanel(
ret.Add((type, localPre, post, name, border)); ret.Add((type, localPre, post, name, border));
} }
} }
public ReadOnlySpan<byte> Id
=> "cp"u8;
public void Draw()
{
switch (config.Ephemeral.CollectionPanel)
{
case CollectionPanelMode.SimpleAssignment: DrawSimple(); break;
case CollectionPanelMode.IndividualAssignment: DrawIndividualPanel(); break;
case CollectionPanelMode.GroupAssignment: DrawGroupPanel(); break;
case CollectionPanelMode.Details: DrawDetailsPanel(); break;
}
}
} }

View file

@ -1,5 +1,5 @@
using ImSharp; using ImSharp;
using OtterGui; using Luna;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
@ -9,87 +9,20 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable public sealed class CollectionSelector(ActiveCollections active, TutorialService tutorial, IncognitoService incognito) : IPanel
{ {
private readonly Configuration _config; public ReadOnlySpan<byte> Id
private readonly CommunicatorService _communicator; => "##cs"u8;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito;
private ModCollection? _dragging; private ModCollection? _dragging;
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, public record struct Entry(ModCollection Collection, StringU8 Name, StringPair AnonymousName)
TutorialService tutorial, IncognitoService incognito)
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{ {
_config = config; public Entry(ModCollection collection)
_communicator = communicator; : this(collection,
_storage = storage; collection.Identity.Name.Length > 0 ? new StringU8(collection.Identity.Name) : new StringU8(collection.Identity.AnonymizedName),
_active = active; new StringPair(collection.Identity.AnonymizedName))
_tutorial = tutorial; { }
_incognito = incognito;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
// Set items.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
// Set selection.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
}
protected override bool OnDelete(int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
// Always return false since we handle the selection update ourselves.
_storage.RemoveCollection(Items[idx]);
return false;
}
protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current
? $"The selected collection {Name(Current)} can not be deleted."
: $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.AddCollection(name, Items[idx]);
}
protected override bool Filtered(int idx)
=> !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
protected override bool OnDraw(int idx)
{
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
using var source = Im.DragDrop.Source();
if (idx == CurrentIdx)
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
if (source)
{
_dragging = Items[idx];
source.SetPayload("Collection"u8);
Im.Text($"Assigning {Name(_dragging)} to...");
}
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
return ret;
} }
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
@ -98,45 +31,72 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8)) if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
return; return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); active.SetCollection(_dragging, type, active.Individuals.GetGroup(identifier));
_dragging = null; _dragging = null;
} }
public void Dispose() public void Draw()
{ {
_communicator.CollectionChange.Unsubscribe(OnCollectionChange); Im.Cursor.Y += Im.Style.FramePadding.Y;
} var cache = CacheManager.Instance.GetOrCreateCache<Cache>(Im.Id.Current);
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
private string Name(ModCollection collection) foreach (var item in cache)
=> _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
public void RestoreCollections()
{
Items.Clear();
Items.Add(_storage.DefaultNamed);
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
Items.Add(c);
SetFilterDirty();
SetCurrent(_active.Current);
}
private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
switch (arguments.Type)
{ {
case CollectionType.Temporary: return; Im.Cursor.X += Im.Style.FramePadding.X;
case CollectionType.Current: var ret = Im.Selectable(incognito.IncognitoMode ? item.AnonymousName : item.Name, active.Current == item.Collection);
if (arguments.NewCollection is not null) using var source = Im.DragDrop.Source();
SetCurrent(arguments.NewCollection);
SetFilterDirty(); if (active.Current == item.Collection)
return; tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
case CollectionType.Inactive:
RestoreCollections(); if (source)
SetFilterDirty(); {
return; _dragging = item.Collection;
default: source.SetPayload("Collection"u8);
SetFilterDirty(); Im.Text($"Assigning {(incognito.IncognitoMode ? item.AnonymousName : item.Name)} to...");
return; }
if (ret)
active.SetCollection(item.Collection, CollectionType.Current);
}
}
public sealed class Cache : BasicFilterCache<Entry>, IService
{
private readonly CollectionStorage _collections;
private readonly CommunicatorService _communicator;
public Cache(CollectionFilter filter, CollectionStorage collections, CommunicatorService communicator)
: base(filter)
{
_collections = collections;
_communicator = communicator;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelectorCache);
_communicator.CollectionRename.Subscribe(OnCollectionRename, CollectionRename.Priority.CollectionSelectorCache);
}
private void OnCollectionRename(in CollectionRename.Arguments arguments)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
if (arguments.Type is CollectionType.Inactive)
Dirty |= IManagedCache.DirtyFlags.Custom;
}
protected override IEnumerable<Entry> GetItems()
{
yield return new Entry(_collections.DefaultNamed);
foreach (var collection in _collections.Where(c => c != _collections.DefaultNamed).OrderBy(c => c.Identity.Name))
yield return new Entry(collection);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_communicator.CollectionRename.Unsubscribe(OnCollectionRename);
} }
} }
} }

View file

@ -0,0 +1,52 @@
using ImSharp;
using Luna;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.UI.ModsTab.Groups;
namespace Penumbra.UI;
public sealed class SingleGroupCombo : FilterComboBase<SingleGroupCombo.Test>, IUiService
{
private class OptionFilter : Utf8FilterBase<Test>
{
protected override ReadOnlySpan<byte> ToFilterString(in Test item, int globalIndex)
=> item.Name;
}
public SingleGroupCombo()
=> Filter = new OptionFilter();
public readonly record struct Test(int OptionIndex, StringU8 Name, StringU8 Description);
private readonly WeakReference<SingleModGroup> _group = new(null!);
private Setting _currentOption;
public void Draw(ModGroupDrawer parent, SingleModGroup group, int groupIndex, Setting currentOption)
{
_currentOption = currentOption;
_group.SetTarget(group);
if (base.Draw(group.Name, group.OptionData[currentOption.AsIndex].Name, StringU8.Empty, UiHelpers.InputTextWidth.X * 3 / 4,
out var newOption))
parent.SetModSetting(group, groupIndex, Setting.Single(newOption.OptionIndex));
}
protected override IEnumerable<Test> GetItems()
=> _group.TryGetTarget(out var target)
? target.OptionData.Select(o => new Test(o.GetIndex(), new StringU8(o.Name), new StringU8(o.Description)))
: [];
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in Test item, int globalIndex, bool selected)
{
var ret = Im.Selectable(item.Name, selected);
if (item.Description.Length > 0)
LunaStyle.DrawHelpMarker(item.Description, treatAsHovered: Im.Item.Hovered());
return ret;
}
protected override bool IsSelected(Test item, int globalIndex)
=> item.OptionIndex == _currentOption.AsIndex;
}

View file

@ -1,118 +0,0 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Widgets;
namespace Penumbra.UI;
public class FilterComboColors : FilterComboCache<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>
{
private readonly float _comboWidth;
private readonly ImRaii.Color _color = new();
private Vector2 _buttonSize;
private uint _currentColor;
private bool _currentGloss;
protected override int UpdateCurrentSelected(int currentSelected)
{
if (CurrentSelection.Value.Color != _currentColor)
{
CurrentSelectionIdx = Items.IndexOf(c => c.Value.Color == _currentColor);
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
return currentSelected;
}
public FilterComboColors(float comboWidth, MouseWheelType allowMouseWheel,
Func<IReadOnlyList<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>> colors,
Logger log)
: base(colors, allowMouseWheel, log)
{
_comboWidth = comboWidth;
SearchByParts = true;
}
protected override float GetFilterWidth()
{
// Hack to not color the filter frame.
_color.Pop();
return _buttonSize.X + ImGui.GetStyle().ScrollbarSize;
}
protected override void DrawList(float width, float itemHeight)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.WindowPadding, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
_buttonSize = new Vector2(_comboWidth * ImGuiHelpers.GlobalScale, 0);
if (ImGui.GetScrollMaxY() > 0)
_buttonSize.X += ImGui.GetStyle().ScrollbarSize;
base.DrawList(width, itemHeight);
}
protected override string ToString(KeyValuePair<byte, (string Name, uint Color, bool Gloss)> obj)
=> obj.Value.Name;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var (_, (name, color, gloss)) = Items[globalIdx];
// Push the stain color to type and if it is too bright, turn the text color black.
var contrastColor = ImGuiUtil.ContrastColorBw(color);
using var colors = ImRaii.PushColor(ImGuiCol.Button, color, color != 0)
.Push(ImGuiCol.Text, contrastColor);
var ret = ImGui.Button(name, _buttonSize);
if (selected)
{
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0xFF2020D0, 0, ImDrawFlags.None,
ImGuiHelpers.GlobalScale);
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin() + new Vector2(ImGuiHelpers.GlobalScale),
ImGui.GetItemRectMax() - new Vector2(ImGuiHelpers.GlobalScale), contrastColor, 0, ImDrawFlags.None, ImGuiHelpers.GlobalScale);
}
if (gloss)
ImGui.GetWindowDrawList().AddRectFilledMultiColor(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0x50FFFFFF, 0x50000000,
0x50FFFFFF, 0x50000000);
return ret;
}
public virtual bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth,
MouseWheelType mouseWheel = MouseWheelType.Control)
{
_currentColor = color;
_currentGloss = gloss;
var preview = found && ImGui.CalcTextSize(name).X <= previewWidth ? name : string.Empty;
AllowMouseWheel = mouseWheel;
_color.Push(ImGuiCol.FrameBg, color, found && color != 0)
.Push(ImGuiCol.Text, ImGuiUtil.ContrastColorBw(color), preview.Length > 0);
var change = Draw(label, preview, found ? name : string.Empty, previewWidth, ImGui.GetFrameHeight(), ImGuiComboFlags.NoArrowButton);
return change;
}
protected override void PostCombo(float previewWidth)
{
_color.Dispose();
if (_currentGloss)
{
var min = ImGui.GetItemRectMin();
ImGui.GetWindowDrawList().AddRectFilledMultiColor(min, new Vector2(min.X + previewWidth, ImGui.GetItemRectMax().Y), 0x50FFFFFF,
0x50000000, 0x50FFFFFF, 0x50000000);
}
}
protected override void OnMouseWheel(string preview, ref int index, int steps)
{
UpdateCurrentSelected(0);
base.OnMouseWheel(preview, ref index, steps);
}
public bool Draw(string label, uint color, string name, bool found, bool gloss,
MouseWheelType mouseWheel = MouseWheelType.Control)
=> Draw(label, color, name, found, gloss, ImGui.GetFrameHeight(), mouseWheel);
}

View file

@ -3,7 +3,6 @@ using ImSharp;
using Luna; using Luna;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType; using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.UI.MainWindow; namespace Penumbra.UI.MainWindow;

View file

@ -1,65 +1,22 @@
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab.Groups;
public sealed class ModGroupDrawer : IUiService public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager, SingleGroupCombo combo)
: IUiService
{ {
private readonly List<(IModGroup, int)> _blockGroupCache = []; private readonly List<(IModGroup, int)> _blockGroupCache = [];
private bool _temporary; private bool _temporary;
private bool _locked; private bool _locked;
private TemporaryModSettings? _tempSettings; private TemporaryModSettings? _tempSettings;
private ModSettings? _settings; private ModSettings? _settings;
private readonly SingleGroupCombo _combo;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
public ModGroupDrawer(Configuration config, CollectionManager collectionManager)
{
_config = config;
_collectionManager = collectionManager;
_combo = new SingleGroupCombo(this);
}
private sealed class SingleGroupCombo(ModGroupDrawer parent)
: FilterComboCache<IModOption>(() => _group!.Options, MouseWheelType.Control, Penumbra.Log)
{
private static IModGroup? _group;
private static int _groupIdx;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var option = _group!.Options[globalIdx];
var ret = Im.Selectable(option.Name, globalIdx == CurrentSelectionIdx);
if (option.Description.Length > 0)
LunaStyle.DrawHelpMarker(option.Description, treatAsHovered: Im.Item.Hovered());
return ret;
}
protected override string ToString(IModOption obj)
=> obj.Name;
public void Draw(IModGroup group, int groupIndex, int currentOption)
{
_group = group;
_groupIdx = groupIndex;
CurrentSelectionIdx = currentOption;
CurrentSelection = _group.Options[CurrentSelectionIdx];
if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4,
Im.Style.TextHeightWithSpacing))
parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx));
}
}
public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings)
{ {
@ -79,7 +36,7 @@ public sealed class ModGroupDrawer : IUiService
switch (group.Behaviour) switch (group.Behaviour)
{ {
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax: case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax:
case GroupDrawBehaviour.MultiSelection: case GroupDrawBehaviour.MultiSelection:
_blockGroupCache.Add((group, idx)); _blockGroupCache.Add((group, idx));
break; break;
@ -120,9 +77,8 @@ public sealed class ModGroupDrawer : IUiService
private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting)
{ {
using var id = Im.Id.Push(groupIdx); using var id = Im.Id.Push(groupIdx);
var selectedOption = setting.AsIndex;
using var disabled = Im.Disabled(_locked); using var disabled = Im.Disabled(_locked);
_combo.Draw(group, groupIdx, selectedOption); combo.Draw(this, (SingleModGroup)group, groupIdx, setting);
if (group.Description.Length > 0) if (group.Description.Length > 0)
{ {
LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description); LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description);
@ -227,7 +183,7 @@ public sealed class ModGroupDrawer : IUiService
private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw) private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw)
{ {
if (options.Count <= _config.OptionGroupCollapsibleMin) if (options.Count <= config.OptionGroupCollapsibleMin)
{ {
draw(); draw();
} }
@ -272,21 +228,21 @@ public sealed class ModGroupDrawer : IUiService
} }
private ModCollection Current private ModCollection Current
=> _collectionManager.Active.Current; => collectionManager.Active.Current;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void SetModSetting(IModGroup group, int groupIdx, Setting setting) internal void SetModSetting(IModGroup group, int groupIdx, Setting setting)
{ {
if (_temporary || _config.DefaultTemporaryMode) if (_temporary || config.DefaultTemporaryMode)
{ {
_tempSettings ??= new TemporaryModSettings(group.Mod, _settings); _tempSettings ??= new TemporaryModSettings(group.Mod, _settings);
_tempSettings!.ForceInherit = false; _tempSettings!.ForceInherit = false;
_tempSettings!.Settings[groupIdx] = setting; _tempSettings!.Settings[groupIdx] = setting;
_collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
} }
else else
{ {
_collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting);
} }
} }
} }

View file

@ -1,7 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Widgets;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;

View file

@ -120,7 +120,15 @@ public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
} }
public override void Update() public override void Update()
{ } {
if (ColorsDirty)
{
CollapsedFolderColor = ColorId.FolderCollapsed.Value().ToVector();
ExpandedFolderColor = ColorId.FolderExpanded.Value().ToVector();
LineColor = ColorId.FolderLine.Value().ToVector();
Dirty &= ~IManagedCache.DirtyFlags.Colors;
}
}
protected override ModData ConvertNode(in IFileSystemNode node) protected override ModData ConvertNode(in IFileSystemNode node)
=> new((IFileSystemData<Mod>)node); => new((IFileSystemData<Mod>)node);

View file

@ -0,0 +1,90 @@
using ImSharp;
using Luna;
using Penumbra.Collections.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public sealed class CollectionButtonFooter : ButtonFooter
{
public CollectionButtonFooter(CollectionManager collectionManager, CommunicatorService communicator, Configuration configuration,
TutorialService tutorial, IncognitoService incognito)
{
Buttons.AddButton(new AddButton(collectionManager.Storage), 100);
Buttons.AddButton(new DuplicateButton(collectionManager.Storage, collectionManager.Active), 50);
Buttons.AddButton(new DeleteButton(collectionManager.Storage, collectionManager.Active, configuration), 0);
}
public sealed class AddButton(CollectionStorage collections) : BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.AddObjectIcon;
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text("Add a new, empty collection."u8);
public override void OnClick()
=> Im.Popup.Open("NewCollection"u8);
protected override void PostDraw()
{
if (!InputPopup.OpenName("NewCollection"u8, out var newCollectionName))
return;
collections.AddCollection(newCollectionName, null);
}
}
public sealed class DeleteButton(CollectionStorage collections, ActiveCollections active, Configuration config)
: BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.DeleteIcon;
public override bool HasTooltip
=> true;
public override bool Enabled
=> collections.DefaultNamed != active.Current
&& config.DeleteModModifier.IsActive();
public override void DrawTooltip()
{
Im.Text("Delete the current collection."u8);
if (collections.DefaultNamed == active.Current)
Im.Text("The default collection cannot be deleted."u8);
else if (!config.DeleteModModifier.IsActive())
Im.Text($"Hold {config.DeleteModModifier} to delete the current collection.");
}
public override void OnClick()
=> collections.RemoveCollection(active.Current);
}
public sealed class DuplicateButton(CollectionStorage collections, ActiveCollections active) : BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.DuplicateIcon;
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text("Duplicate the currently selected collection to a new one."u8);
public override void OnClick()
=> Im.Popup.Open("DuplicateCollection"u8);
protected override void PostDraw()
{
if (!InputPopup.OpenName("DuplicateCollection"u8, out var newCollectionName))
return;
collections.AddCollection(newCollectionName, active.Current);
}
}
}

View file

@ -0,0 +1,66 @@
using ImSharp;
using Luna;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public enum CollectionPanelMode
{
SimpleAssignment,
IndividualAssignment,
GroupAssignment,
Details,
};
public sealed class CollectionModeHeader(Configuration config, TutorialService tutorial, IncognitoService incognito) : IHeader
{
public bool Collapsed
=> false;
private CollectionPanelMode Mode
{
get => config.Ephemeral.CollectionPanel;
set
{
config.Ephemeral.CollectionPanel = value;
config.Ephemeral.Save();
}
}
public void Draw(Vector2 size)
{
var withSpacing = Im.Style.FrameHeightWithSpacing;
var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight);
var tabSelectedColor = Im.Style[ImGuiColor.TabSelected];
using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is CollectionPanelMode.SimpleAssignment);
if (Im.Button("Simple Assignments"u8, buttonSize))
Mode = CollectionPanelMode.SimpleAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.IndividualAssignment);
if (Im.Button("Individual Assignments"u8, buttonSize))
Mode = CollectionPanelMode.IndividualAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.GroupAssignment);
if (Im.Button("Group Assignments"u8, buttonSize))
Mode = CollectionPanelMode.GroupAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.Details);
if (Im.Button("Collection Details"u8, buttonSize))
Mode = CollectionPanelMode.Details;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
Im.Line.NoSpacing();
incognito.DrawToggle(withSpacing);
}
}

View file

@ -1,151 +1,42 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.CollectionTab; using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.Tabs;
public sealed class CollectionsTab : ITab<TabType>, IDisposable public sealed class CollectionsTab : TwoPanelLayout, ITab<TabType>
{ {
private readonly EphemeralConfig _config; private readonly TutorialService _tutorial;
private readonly CollectionSelector _selector;
private readonly CollectionPanel _panel;
private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito;
public enum PanelMode
{
SimpleAssignment,
IndividualAssignment,
GroupAssignment,
Details,
};
public PanelMode Mode
{
get => _config.CollectionPanel;
set
{
_config.CollectionPanel = value;
_config.Save();
}
}
public TabType Identifier public TabType Identifier
=> TabType.Collections; => TabType.Collections;
public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, public CollectionsTab(TutorialService tutorial, CollectionButtonFooter leftFooter, CollectionSelector leftPanel, CollectionFilter filter,
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) CollectionModeHeader rightHeader, CollectionPanel rightPanel)
{ {
_config = configuration.Ephemeral; LeftHeader = new FilterHeader<CollectionSelector.Entry>(filter, new StringU8("Filter..."u8));
_tutorial = tutorial; LeftPanel = leftPanel;
_incognito = incognito; LeftFooter = leftFooter;
_selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); RightHeader = rightHeader;
_panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito); RightPanel = rightPanel;
RightFooter = NopHeaderFooter.Instance;
_tutorial = tutorial;
} }
public void Dispose() public override ReadOnlySpan<byte> Label
{
_selector.Dispose();
_panel.Dispose();
}
public ReadOnlySpan<byte> Label
=> "Collections"u8; => "Collections"u8;
public void DrawContent() protected override void DrawLeftGroup()
{ {
var width = Im.Font.CalculateSize("nnnnnnnnnnnnnnnnnnnnnnnnnn"u8).X; base.DrawLeftGroup();
using (Im.Group())
{
_selector.Draw(width);
}
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
Im.Line.Same();
using (Im.Group())
{
DrawHeaderLine();
DrawPanel();
}
} }
public void DrawHeader() public void DrawContent()
{ => Draw();
_tutorial.OpenTutorial(BasicTutorialSteps.Collections);
}
private void DrawHeaderLine() public void PostTabButton()
{ => _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
var withSpacing = Im.Style.FrameHeightWithSpacing;
using var style = ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight);
using var _ = Im.Group();
var tabSelectedColor = Im.Style[ImGuiColor.TabSelected];
using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is PanelMode.SimpleAssignment);
if (Im.Button("Simple Assignments"u8, buttonSize))
Mode = PanelMode.SimpleAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.IndividualAssignment);
if (Im.Button("Individual Assignments"u8, buttonSize))
Mode = PanelMode.IndividualAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.GroupAssignment);
if (Im.Button("Group Assignments"u8, buttonSize))
Mode = PanelMode.GroupAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.Details);
if (Im.Button("Collection Details"u8, buttonSize))
Mode = PanelMode.Details;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
Im.Line.Same();
_incognito.DrawToggle(withSpacing);
}
private void DrawPanel()
{
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
using var child = Im.Child.Begin("##CollectionSettings"U8, Im.ContentRegion.Available with { Y = 0 }, true);
if (!child)
return;
style.Pop();
switch (Mode)
{
case PanelMode.SimpleAssignment:
_panel.DrawSimple();
break;
case PanelMode.IndividualAssignment:
_panel.DrawIndividualPanel();
break;
case PanelMode.GroupAssignment:
_panel.DrawGroupPanel();
break;
case PanelMode.Details:
_panel.DrawDetailsPanel();
break;
}
style.Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
}
} }

View file

@ -0,0 +1,51 @@
using ImSharp;
using Luna;
using Penumbra.Interop.Services;
using Penumbra.String;
namespace Penumbra.UI.Tabs.Debug;
public sealed class ActionTmbListDrawer(SchedulerResourceManagementService service) : IUiService
{
public readonly SchedulerResourceManagementService Service = service;
public readonly IFilter<TmbEntry> KeyFilter = new TmbKeyFilter();
public sealed class Cache(ActionTmbListDrawer parent) : BasicFilterCache<TmbEntry>(parent.KeyFilter)
{
protected override IEnumerable<TmbEntry> GetItems()
=> parent.Service.ActionTmbs.OrderBy(t => t.Value).Select(k => new TmbEntry(k.Key, k.Value));
}
public readonly struct TmbEntry(CiByteString key, uint value)
{
public readonly StringPair Key = new(key.ToString());
public readonly StringU8 Value = new($"{value}");
public void Draw()
{
Im.Table.DrawColumn(Value);
Im.Table.DrawColumn(Key.Utf8);
}
}
public sealed class TmbKeyFilter : RegexFilterBase<TmbEntry>
{
protected override string ToFilterString(in TmbEntry item, int globalIndex)
=> item.Key.Utf16;
}
public void Draw()
{
KeyFilter.DrawFilter("Key"u8, Im.ContentRegion.Available);
using var table = Im.Table.Begin("##table"u8, 2,
TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit,
Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing });
if (!table)
return;
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this));
using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing);
foreach (var tmb in clip.Iterate(cache))
tmb.Draw();
}
}

View file

@ -1,12 +1,10 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using ImSharp; using ImSharp;
using Luna; using Luna;
@ -15,7 +13,6 @@ using Penumbra.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
@ -28,7 +25,6 @@ using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester; using Penumbra.Api.IpcTester;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.PostProcessing;
@ -65,54 +61,54 @@ public class Diagnostics(ServiceManager provider) : IUiService
public sealed class DebugTab : Window, ITab<TabType> public sealed class DebugTab : Window, ITab<TabType>
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker; private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi; private readonly HttpApi _httpApi;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly StainService _stains; private readonly StainService _stains;
private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly GlobalVariablesDrawer _globalVariablesDrawer;
private readonly ResourceManagerService _resourceManager; private readonly ResourceManagerService _resourceManager;
private readonly ResourceLoader _resourceLoader; private readonly ResourceLoader _resourceLoader;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState; private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState; private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper; private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService; private readonly CutsceneService _cutsceneService;
private readonly ModImportManager _modImporter; private readonly ModImportManager _modImporter;
private readonly ImportPopup _importPopup; private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework; private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager; private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws; private readonly RedrawService _redraws;
private readonly DictEmote _emotes; private readonly EmoteListDrawer _emotes;
private readonly Diagnostics _diagnostics; private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects; private readonly ObjectManager _objects;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester; private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel; private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer; private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides; private readonly HookOverrideDrawer _hookOverrides;
private readonly RsfService _rsfService; private readonly RsfService _rsfService;
private readonly SchedulerResourceManagementService _schedulerService; private readonly ActionTmbListDrawer _actionTmbs;
private readonly ObjectIdentification _objectIdentification; private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetDrawer _renderTargetDrawer; private readonly RenderTargetDrawer _renderTargetDrawer;
private readonly ModMigratorDebug _modMigratorDebug; private readonly ModMigratorDebug _modMigratorDebug;
private readonly ShapeInspector _shapeInspector; private readonly ShapeInspector _shapeInspector;
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer; private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
private readonly DragDropManager _dragDropManager; private readonly DragDropManager _dragDropManager;
public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager, public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, EmoteListDrawer emotes,
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, ActionTmbListDrawer actionTmbs, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer, ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer,
DragDropManager dragDropManager) DragDropManager dragDropManager)
: base("Penumbra Debug Window", WindowFlags.NoCollapse) : base("Penumbra Debug Window", WindowFlags.NoCollapse)
@ -152,7 +148,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_hookOverrides = hookOverrides; _hookOverrides = hookOverrides;
_rsfService = rsfService; _rsfService = rsfService;
_globalVariablesDrawer = globalVariablesDrawer; _globalVariablesDrawer = globalVariablesDrawer;
_schedulerService = schedulerService; _actionTmbs = actionTmbs;
_objectIdentification = objectIdentification; _objectIdentification = objectIdentification;
_renderTargetDrawer = renderTargetDrawer; _renderTargetDrawer = renderTargetDrawer;
_modMigratorDebug = modMigratorDebug; _modMigratorDebug = modMigratorDebug;
@ -205,7 +201,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_globalVariablesDrawer.Draw(); _globalVariablesDrawer.Draw();
DrawCloudApi(); DrawCloudApi();
DrawDebugTabIpc(); DrawDebugTabIpc();
if(Im.Tree.Header("Drag & Drop Manager"u8)) if (Im.Tree.Header("Drag & Drop Manager"u8))
_dragDropManager.DrawDebugInfo(); _dragDropManager.DrawDebugInfo();
} }
@ -742,7 +738,7 @@ public sealed class DebugTab : Window, ITab<TabType>
{ {
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit); using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key)) foreach (var (id, name) in _actionTmbs.Service.ListedTmbs.OrderBy(kvp => kvp.Key))
table.DrawDataPair($"{id:D6}", name.Span); table.DrawDataPair($"{id:D6}", name.Span);
} }
} }
@ -814,11 +810,6 @@ public sealed class DebugTab : Window, ITab<TabType>
Im.Selectable(item.Key); Im.Selectable(item.Key);
} }
private string _emoteSearchFile = string.Empty;
private string _emoteSearchName = string.Empty;
private AtchFile? _atchFile; private AtchFile? _atchFile;
private void DrawAtch() private void DrawAtch()
@ -842,57 +833,19 @@ public sealed class DebugTab : Window, ITab<TabType>
AtchDrawer.Draw(_atchFile); AtchDrawer.Draw(_atchFile);
} }
private void DrawEmotes() private void DrawEmotes()
{ {
using var mainTree = Im.Tree.Node("Emotes"u8); using var mainTree = Im.Tree.Node("Emotes"u8);
if (!mainTree) if (mainTree)
return; _emotes.Draw();
Im.Input.Text("File Name"u8, ref _emoteSearchFile);
Im.Input.Text("Emote Name"u8, ref _emoteSearchName);
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips,
p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase)
&& (_emoteSearchName.Length == 0
|| p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))),
p =>
{
Im.Table.DrawColumn(p.Key);
Im.Table.DrawColumn(StringU8.Join(", "u8, p.Value.Select(v => v.Name.ToDalamudString().TextValue)));
});
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
} }
private string _tmbKeyFilter = string.Empty;
private CiByteString _tmbKeyFilterU8 = CiByteString.Empty;
private void DrawActionTmbs() private void DrawActionTmbs()
{ {
using var mainTree = Im.Tree.Node("Action TMBs"u8); using var mainTree = Im.Tree.Node("Action TMBs"u8);
if (!mainTree) if (mainTree)
return; _actionTmbs.Draw();
if (Im.Input.Text("Key"u8, ref _tmbKeyFilter))
_tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty;
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips,
kvp => kvp.Key.Contains(_tmbKeyFilterU8),
p =>
{
Im.Table.DrawColumn($"{p.Value}");
Im.Table.DrawColumn(p.Key.Span);
});
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
} }
private void DrawStainTemplates() private void DrawStainTemplates()

View file

@ -0,0 +1,79 @@
using ImSharp;
using Lumina.Excel.Sheets;
using Luna;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
namespace Penumbra.UI.Tabs.Debug;
public sealed class EmoteListDrawer(DictEmote emotes) : IUiService
{
public readonly DictEmote Emotes = emotes;
public readonly IFilter<EmoteEntry> FileFilter = new EmoteFileFilter();
public readonly IFilter<EmoteEntry> NameFilter = new EmoteNameFilter();
public sealed class Cache(EmoteListDrawer parent)
: BasicFilterCache<EmoteEntry>(new PairFilter<EmoteEntry>(parent.FileFilter, parent.NameFilter))
{
protected override IEnumerable<EmoteEntry> GetItems()
=> parent.Emotes.Value.Select(kvp => new EmoteEntry(kvp.Key, kvp.Value));
}
public sealed class EmoteFileFilter : RegexFilterBase<EmoteEntry>
{
protected override string ToFilterString(in EmoteEntry item, int globalIndex)
=> item.File.Utf16;
}
public sealed class EmoteNameFilter : RegexFilterBase<EmoteEntry>
{
public override bool WouldBeVisible(in EmoteEntry item, int globalIndex)
=> Text.Length is 0 || item.Emotes.Any(e => WouldBeVisible(e.Utf16));
protected override string ToFilterString(in EmoteEntry item, int globalIndex)
=> string.Empty;
}
public readonly struct EmoteEntry
{
public readonly StringPair File;
public readonly List<StringPair> Emotes;
public EmoteEntry(string key, IReadOnlyList<Emote> emotes)
{
File = new StringPair(key);
Emotes = emotes.Select(e => new StringPair(e.Name.ExtractTextExtended())).ToList();
}
public void Draw()
{
Im.Table.DrawColumn(File.Utf8);
if (Emotes.Count > 0)
Im.Table.DrawColumn(Emotes[0].Utf8);
foreach (var emote in Emotes.Skip(1))
{
Im.Line.NoSpacing();
Im.Text(", "u8);
Im.Line.NoSpacing();
Im.Text(emote.Utf16);
}
}
}
public void Draw()
{
FileFilter.DrawFilter("File Name"u8, Im.ContentRegion.Available);
NameFilter.DrawFilter("Emote Name"u8, Im.ContentRegion.Available);
using var table = Im.Table.Begin("##table"u8, 2,
TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit,
Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing });
if (!table)
return;
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this));
using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing);
foreach (var emote in clip.Iterate(cache))
emote.Draw();
}
}