Use Luna tabbing.
Some checks failed
.NET Build / build (push) Has been cancelled

This commit is contained in:
Ottermandias 2025-11-03 20:54:54 +01:00
parent 5a2fddab89
commit 9aa1121410
28 changed files with 616 additions and 719 deletions

2
Luna

@ -1 +1 @@
Subproject commit 2e984d9c21370c778d172ab955def18c0dbe8c7d
Subproject commit cb294f476476f7a3d8b56a0072dbd300b3d54c4f

View file

@ -10,7 +10,7 @@ public sealed class SelectTab(Logger log) : EventBase<SelectTab.Arguments, Selec
public enum Priority
{
/// <seealso cref="UI.Tabs.ConfigTabBar.OnSelectTab"/>
ConfigTabBar = 0,
MainTabBar = 0,
}
/// <summary> The arguments for a SelectTab event. </summary>

View file

@ -10,6 +10,7 @@ using Penumbra.UI;
using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Tabs;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra;

View file

@ -134,7 +134,7 @@ public class Penumbra : IDalamudPlugin
AsyncTask.Run(() =>
{
var system = _services.GetService<PenumbraWindowSystem>();
system.Window.Setup(this, _services.GetService<ConfigTabBar>());
system.Window.Setup(this, _services.GetService<MainTabBar>());
_services.GetService<CommandHandler>();
if (!_disposed)
{
@ -199,8 +199,12 @@ public class Penumbra : IDalamudPlugin
{
ReadOnlySpan<string> relevantPlugins =
[
"Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge",
"IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn",
"Glamourer", "CustomizePlus", "SimpleHeels",
"Ktisis", "Brio",
"heliosphere-plugin", "VfxEditor", "IllusioVitae", "Aetherment",
"DynamicBridge", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn",
"MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync", "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni"
];
var plugins = _services.GetService<IDalamudPluginInterface>().InstalledPlugins
.GroupBy(p => p.InternalName)

View file

@ -14,6 +14,7 @@ using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.Services;

View file

@ -4,7 +4,6 @@ using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
@ -25,6 +24,7 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
using ITab = OtterGui.Widgets.ITab;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow;

View file

@ -1,10 +1,10 @@
using Dalamud.Plugin;
using ImSharp;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.UI;
@ -14,7 +14,7 @@ public sealed class ConfigWindow : Window
private readonly Configuration _config;
private readonly ValidityChecker _validityChecker;
private Penumbra? _penumbra;
private ConfigTabBar _configTabs = null!;
private MainTabBar _configTabs = null!;
private string? _lastException;
public ConfigWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker,
@ -32,15 +32,15 @@ public sealed class ConfigWindow : Window
public void OpenSettings()
{
_configTabs.SelectTab = TabType.Settings;
IsOpen = true;
_configTabs.NextTab = TabType.Settings;
IsOpen = true;
}
public void Setup(Penumbra penumbra, ConfigTabBar configTabs)
public void Setup(Penumbra penumbra, MainTabBar configTabs)
{
_penumbra = penumbra;
_configTabs = configTabs;
_configTabs.SelectTab = _config.Ephemeral.SelectedTab;
_penumbra = penumbra;
_configTabs = configTabs;
_configTabs.NextTab = _config.Ephemeral.SelectedTab;
}
public override bool DrawConditions()
@ -98,12 +98,7 @@ public sealed class ConfigWindow : Window
}
else
{
var type = _configTabs.Draw();
if (type != _config.Ephemeral.SelectedTab)
{
_config.Ephemeral.SelectedTab = type;
_config.Ephemeral.Save();
}
_configTabs.Draw();
}
_lastException = null;

View file

@ -15,18 +15,18 @@ public class IncognitoService(TutorialService tutorial, Configuration config) :
var color = ColorId.FolderExpanded.Value();
using (ImStyleBorder.Frame.Push(color))
{
var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8;
var icon = IncognitoMode ? LunaStyle.IncognitoOn : LunaStyle.IncognitoOff;
var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8;
var icon = IncognitoMode ? LunaStyle.IncognitoOn : LunaStyle.IncognitoOff;
if (ImEx.Icon.Button(icon, tt, size: new Vector2(width, Im.Style.FrameHeight), textColor: color) && hold)
{
config.Ephemeral.IncognitoMode = !IncognitoMode;
config.Ephemeral.Save();
}
if (!hold)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle.");
}
if (!hold)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle.");
tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
}
}

View file

@ -6,7 +6,6 @@ using Luna;
using OtterGui;
using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -23,7 +22,7 @@ public class ModPanelChangedItemsTab(
ImGuiCacheService cacheService,
Configuration config,
ModDataEditor dataEditor)
: ITab, Luna.IUiService
: ITab<ModPanelTab>
{
private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId();
@ -209,6 +208,9 @@ public class ModPanelChangedItemsTab(
public ReadOnlySpan<byte> Label
=> "Changed Items"u8;
public ModPanelTab Identifier
=> ModPanelTab.ChangedItems;
public bool IsVisible
=> selector.Selected!.ChangedItems.Count > 0;

View file

@ -1,9 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using ImSharp;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Luna;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
@ -11,7 +8,7 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab;
public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab, Luna.IUiService
public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab<ModPanelTab>
{
private enum ModState
{
@ -24,19 +21,22 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
public ReadOnlySpan<byte> Label
=> "Collections"u8;
public ModPanelTab Identifier
=> ModPanelTab.Collections;
public void DrawContent()
{
var (direct, inherited) = CountUsage(selector.Selected!);
Im.Line.New();
if (direct == 1)
ImUtf8.Text("This Mod is directly configured in 1 collection."u8);
else if (direct == 0)
ImUtf8.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder);
else
ImUtf8.Text($"This Mod is directly configured in {direct} collections.");
switch (direct)
{
case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break;
case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break;
default: Im.Text($"This Mod is directly configured in {direct} collections."); break;
}
if (inherited > 0)
ImUtf8.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance.");
Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance.");
Im.Line.New();
Im.Separator();
@ -45,7 +45,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
if (!table)
return;
var size = ImUtf8.CalcTextSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale;
var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale;
var collectionSize = 200 * Im.Style.GlobalScale;
table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize);
table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size);
@ -54,21 +54,21 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
ImGui.TableHeadersRow();
foreach (var (idx, (collection, parent, color, state)) in _cache.Index())
{
using var id = ImUtf8.PushId(idx);
ImUtf8.DrawTableColumn(collection.Identity.Name);
using var id = Im.Id.Push(idx);
table.DrawColumn(collection.Identity.Name);
ImGui.TableNextColumn();
ImUtf8.Text(ToText(state), color);
table.NextColumn();
Im.Text(ToText(state), color);
using (var context = ImUtf8.PopupContextItem("Context"u8))
using (var context = Im.Popup.BeginContextItem("Context"u8))
{
if (context)
{
ImUtf8.Text(collection.Identity.Name);
Im.Text(collection.Identity.Name);
Im.Separator();
using (ImRaii.Disabled(state is ModState.Enabled && parent == collection))
using (Im.Disabled(state is ModState.Enabled && parent == collection))
{
if (ImUtf8.MenuItem("Enable"u8))
if (Im.Menu.Item("Enable"u8))
{
if (parent != collection)
manager.Editor.SetModInheritance(collection, selector.Selected!, false);
@ -76,9 +76,9 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
}
}
using (ImRaii.Disabled(state is ModState.Disabled && parent == collection))
using (Im.Disabled(state is ModState.Disabled && parent == collection))
{
if (ImUtf8.MenuItem("Disable"u8))
if (Im.Menu.Item("Disable"u8))
{
if (parent != collection)
manager.Editor.SetModInheritance(collection, selector.Selected!, false);
@ -86,15 +86,15 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
}
}
using (ImRaii.Disabled(parent != collection))
using (Im.Disabled(parent != collection))
{
if (ImUtf8.MenuItem("Inherit"u8))
if (Im.Menu.Item("Inherit"u8))
manager.Editor.SetModInheritance(collection, selector.Selected!, true);
}
}
}
ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Identity.Name);
table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name);
}
}

View file

@ -13,11 +13,14 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab;
public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab, IUiService
public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab<ModPanelTab>
{
public ReadOnlySpan<byte> Label
=> "Conflicts"u8;
public ModPanelTab Identifier
=> ModPanelTab.Conflicts;
public bool IsVisible
=> collectionManager.Active.Current.Conflicts(selector.Selected!).Any(c => !GetPriority(c).IsHidden);

View file

@ -1,7 +1,6 @@
using Dalamud.Bindings.ImGui;
using ImSharp;
using OtterGui.Raii;
using OtterGui;
using Luna;
using OtterGui.Widgets;
using Penumbra.Mods.Manager;
@ -12,23 +11,25 @@ public class ModPanelDescriptionTab(
TutorialService tutorial,
ModManager modManager,
PredefinedTagManager predefinedTagsConfig)
: ITab, Luna.IUiService
: ITab<ModPanelTab>
{
private readonly TagButtons _localTags = new();
private readonly TagButtons _modTags = new();
public ReadOnlySpan<byte> Label
=> "Description"u8;
public ModPanelTab Identifier
=> ModPanelTab.Description;
public void DrawContent()
{
using var child = ImRaii.Child("##description");
using var child = Im.Child.Begin("##description"u8);
if (!child)
return;
ImGui.Dummy(ImEx.ScaledVector(2));
ImGui.Dummy(ImEx.ScaledVector(2));
Im.ScaledDummy(2, 2);
Im.ScaledDummy(2, 2);
var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled
? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? Im.Style.ScrollbarSize : 0))
: (false, 0);
@ -49,9 +50,9 @@ public class ModPanelDescriptionTab(
selector.Selected!.ModTags, out _, false,
ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X);
ImGui.Dummy(ImEx.ScaledVector(2));
Im.ScaledDummy(2, 2);
Im.Separator();
ImGuiUtil.TextWrapped(selector.Selected!.Description);
Im.TextWrapped(selector.Selected!.Description);
}
}

View file

@ -1,370 +1,335 @@
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Bindings.ImGui;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using OtterGui.Text;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Mods.Settings;
using Penumbra.UI.ModsTab.Groups;
namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab(
ModManager modManager,
ModFileSystemSelector selector,
ModFileSystem fileSystem,
Services.MessageService messager,
FilenameService filenames,
ModExportManager modExportManager,
Configuration config,
PredefinedTagManager predefinedTagManager,
ModGroupEditDrawer groupEditDrawer,
DescriptionEditPopup descriptionPopup,
AddGroupDrawer addGroupDrawer)
: ITab, IUiService
{
private readonly TagButtons _modTags = new();
private ModFileSystem.Leaf _leaf = null!;
private Mod _mod = null!;
public ReadOnlySpan<byte> Label
=> "Edit Mod"u8;
public void DrawContent()
{
using var child = ImRaii.Child("##editChild", -Vector2.One);
if (!child)
return;
_leaf = selector.SelectedLeaf!;
_mod = selector.Selected!;
EditButtons();
EditRegularMeta();
UiHelpers.DefaultLineSpace();
EditLocalData();
UiHelpers.DefaultLineSpace();
if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X))
try
{
fileSystem.RenameAndMove(_leaf, newPath);
}
catch (Exception e)
{
messager.NotificationMessage(e.Message, NotificationType.Warning, false);
}
UiHelpers.DefaultLineSpace();
FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X);
UiHelpers.DefaultLineSpace();
var sharedTagsEnabled = predefinedTagManager.Enabled;
var sharedTagButtonOffset = sharedTagsEnabled ? Im.Style.FrameHeight + Im.Style.FramePadding.X : 0;
var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags,
out var editedTag, rightEndOffset: sharedTagButtonOffset);
if (tagIdx >= 0)
modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
if (sharedTagsEnabled)
predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false,
selector.Selected!);
UiHelpers.DefaultLineSpace();
addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X);
UiHelpers.DefaultLineSpace();
groupEditDrawer.Draw(_mod);
descriptionPopup.Draw();
}
public void Reset()
{
MoveDirectory.Reset();
Input.Reset();
}
/// <summary> The general edit row for non-detailed mod edits. </summary>
private void EditButtons()
{
var buttonSize = new Vector2(150 * Im.Style.GlobalScale, 0);
var folderExists = Directory.Exists(_mod.ModPath.FullName);
var tt = folderExists
? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice."
: $"Mod directory \"{_mod.ModPath.FullName}\" does not exist.";
if (ImGuiUtil.DrawDisabledButton("Open Mod Directory", buttonSize, tt, !folderExists))
Process.Start(new ProcessStartInfo(_mod.ModPath.FullName) { UseShellExecute = true });
Im.Line.Same();
if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n"
+ "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.",
false))
modManager.ReloadMod(_mod);
BackupButtons(buttonSize);
MoveDirectory.Draw(modManager, _mod, buttonSize);
UiHelpers.DefaultLineSpace();
}
private void BackupButtons(Vector2 buttonSize)
{
var backup = new ModBackup(modExportManager, _mod);
var tt = ModBackup.CreatingBackup
? "Already exporting a mod."
: backup.Exists
? $"Overwrite current exported mod \"{backup.Name}\" with current mod."
: $"Create exported archive of current mod at \"{backup.Name}\".";
if (ImUtf8.ButtonEx("Export Mod"u8, tt, buttonSize, ModBackup.CreatingBackup))
backup.CreateAsync();
if (Im.Item.RightClicked())
ImUtf8.OpenPopup("context"u8);
Im.Line.Same();
tt = backup.Exists
? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)."
: $"Exported mod \"{backup.Name}\" does not exist.";
if (ImUtf8.ButtonEx("Delete Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive()))
backup.Delete();
tt = backup.Exists
? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)."
: $"Exported mod \"{backup.Name}\" does not exist.";
Im.Line.Same();
if (ImUtf8.ButtonEx("Restore From Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive()))
backup.Restore(modManager);
if (backup.Exists)
{
Im.Line.Same();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImUtf8.Text(FontAwesomeIcon.CheckCircle.ToIconString());
}
Im.Tooltip.OnHover($"Export exists in \"{backup.Name}\".");
}
using var context = ImUtf8.Popup("context"u8);
if (!context)
return;
if (ImUtf8.Selectable("Open Backup Directory"u8))
Process.Start(new ProcessStartInfo(modExportManager.ExportDirectory.FullName) { UseShellExecute = true });
}
/// <summary> Anything about editing the regular meta information about the mod. </summary>
private void EditRegularMeta()
{
if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModName(_mod, newName);
if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModAuthor(_mod, newAuthor);
if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32,
UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModVersion(_mod, newVersion);
if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256,
UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModWebsite(_mod, newWebsite);
using var style = ImStyleDouble.ItemSpacing.Push(new Vector2(Im.Style.GlobalScale * 3));
var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0);
if (ImGui.Button("Edit Description", reducedSize))
descriptionPopup.Open(_mod);
Im.Line.Same();
var fileExists = File.Exists(filenames.ModMetaPath(_mod));
var tt = fileExists
? "Open the metadata json file in the text editor of your choice."
: "The metadata json file does not exist.";
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt,
!fileExists, true))
Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true });
DrawOpenDefaultMod();
}
private void EditLocalData()
{
DrawImportDate();
DrawOpenLocalData();
}
private void DrawImportDate()
{
ImEx.TextFramed($"{DateTimeOffset.FromUnixTimeMilliseconds(_mod.ImportDate).ToLocalTime():yyyy/MM/dd HH:mm}",
new Vector2(UiHelpers.InputTextMinusButton3, 0), ImGuiColor.FrameBackground.Get(0.5f));
Im.Line.Same(0, 3 * Im.Style.GlobalScale);
var canRefresh = config.DeleteModModifier.IsActive();
var tt = canRefresh
? "Reset the import date to the current date and time."
: $"Reset the import date to the current date and time.\nHold {config.DeleteModModifier} while clicking to refresh.";
if (ImUtf8.IconButton(FontAwesomeIcon.Sync, tt, disabled: !canRefresh))
modManager.DataEditor.ResetModImportDate(_mod);
Im.Line.SameInner();
ImUtf8.Text("Import Date"u8);
}
private void DrawOpenLocalData()
{
var file = filenames.LocalDataFile(_mod);
var fileExists = File.Exists(file);
var tt = fileExists
? "Open the local mod data file in the text editor of your choice."u8
: "The local mod data file does not exist."u8;
if (ImUtf8.ButtonEx("Open Local Data"u8, tt, UiHelpers.InputTextWidth, !fileExists))
Process.Start(new ProcessStartInfo(file) { UseShellExecute = true });
}
private void DrawOpenDefaultMod()
{
var file = filenames.OptionGroupFile(_mod, -1, false);
var fileExists = File.Exists(file);
var tt = fileExists
? "Open the default mod data file in the text editor of your choice."
: "The default mod data file does not exist.";
if (ImGuiUtil.DrawDisabledButton("Open Default Data", UiHelpers.InputTextWidth, tt, !fileExists))
Process.Start(new ProcessStartInfo(file) { UseShellExecute = true });
}
/// <summary> A text input for the new directory name and a button to apply the move. </summary>
private static class MoveDirectory
{
private static string? _currentModDirectory;
private static NewDirectoryState _state = NewDirectoryState.Identical;
public static void Reset()
{
_currentModDirectory = null;
_state = NewDirectoryState.Identical;
}
public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize)
{
Im.Item.SetNextWidth(buttonSize.X * 2 + Im.Style.ItemSpacing.X);
var tmp = _currentModDirectory ?? mod.ModPath.Name;
if (ImGui.InputText("##newModMove", ref tmp, 64))
{
_currentModDirectory = tmp;
_state = modManager.NewDirectoryValid(mod.ModPath.Name, _currentModDirectory, out _);
}
var (disabled, tt) = _state switch
{
NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
NewDirectoryState.ContainsInvalidSymbols => (true,
$"{_currentModDirectory} contains invalid symbols for FFXIV."),
_ => (true, "Unknown error."),
};
Im.Line.Same();
if (ImGuiUtil.DrawDisabledButton("Rename Mod Directory", buttonSize, tt, disabled) && _currentModDirectory != null)
{
modManager.MoveModDirectory(mod, _currentModDirectory);
Reset();
}
Im.Line.Same();
ImGuiComponents.HelpMarker(
"The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n"
+ "This can currently not be used on pre-existing folders and does not support merges or overwriting.");
}
}
/// <summary> Handles input text and integers in separate fields without buffers for every single one. </summary>
private static class Input
{
// Special field indices to reuse the same string buffer.
public const int None = -1;
public const int Name = -2;
public const int Author = -3;
public const int Version = -4;
public const int Website = -5;
public const int Path = -6;
public const int Description = -7;
// Temporary strings
private static string? _currentEdit;
private static ModPriority? _currentGroupPriority;
private static int _currentField = None;
private static int _optionIndex = None;
public static void Reset()
{
_currentEdit = null;
_currentGroupPriority = null;
_currentField = None;
_optionIndex = None;
}
public static bool Text(string label, int field, int option, string oldValue, out string value, uint maxLength, float width)
{
var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue;
Im.Item.SetNextWidth(width);
if (ImGui.InputText(label, ref tmp))
{
_currentEdit = tmp;
_optionIndex = option;
_currentField = field;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null)
{
var ret = _currentEdit != oldValue;
value = _currentEdit;
Reset();
return ret;
}
value = string.Empty;
return false;
}
public static bool Priority(string label, int field, int option, ModPriority oldValue, out ModPriority value, float width)
{
var tmp = (field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue).Value;
Im.Item.SetNextWidth(width);
if (ImGui.InputInt(label, ref tmp, 0, 0))
{
_currentGroupPriority = new ModPriority(tmp);
_optionIndex = option;
_currentField = field;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null)
{
var ret = _currentGroupPriority != oldValue;
value = _currentGroupPriority.Value;
Reset();
return ret;
}
value = ModPriority.Default;
return false;
}
}
}
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using ImSharp;
using Luna;
using OtterGui.Widgets;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.ModsTab.Groups;
namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab(
ModManager modManager,
ModFileSystemSelector selector,
ModFileSystem fileSystem,
Services.MessageService messager,
FilenameService filenames,
ModExportManager modExportManager,
Configuration config,
PredefinedTagManager predefinedTagManager,
ModGroupEditDrawer groupEditDrawer,
DescriptionEditPopup descriptionPopup,
AddGroupDrawer addGroupDrawer)
: ITab<ModPanelTab>
{
private readonly TagButtons _modTags = new();
private ModFileSystem.Leaf _leaf = null!;
private Mod _mod = null!;
public ReadOnlySpan<byte> Label
=> "Edit Mod"u8;
public ModPanelTab Identifier
=> ModPanelTab.Edit;
public void DrawContent()
{
using var child = Im.Child.Begin("##editChild"u8, Im.ContentRegion.Available);
if (!child)
return;
_leaf = selector.SelectedLeaf!;
_mod = selector.Selected!;
EditButtons();
EditRegularMeta();
UiHelpers.DefaultLineSpace();
EditLocalData();
UiHelpers.DefaultLineSpace();
if (Input.Text("Mod Path"u8, Input.Path, Input.None, _leaf.FullName(), out var newPath, UiHelpers.InputTextWidth.X))
try
{
fileSystem.RenameAndMove(_leaf, newPath);
}
catch (Exception e)
{
messager.NotificationMessage(e.Message, NotificationType.Warning, false);
}
UiHelpers.DefaultLineSpace();
FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X);
UiHelpers.DefaultLineSpace();
var sharedTagsEnabled = predefinedTagManager.Enabled;
var sharedTagButtonOffset = sharedTagsEnabled ? Im.Style.FrameHeight + Im.Style.FramePadding.X : 0;
var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags,
out var editedTag, rightEndOffset: sharedTagButtonOffset);
if (tagIdx >= 0)
modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
if (sharedTagsEnabled)
predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false,
selector.Selected!);
UiHelpers.DefaultLineSpace();
addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X);
UiHelpers.DefaultLineSpace();
groupEditDrawer.Draw(_mod);
descriptionPopup.Draw();
}
public void Reset()
{
MoveDirectory.Reset();
Input.Reset();
}
/// <summary> The general edit row for non-detailed mod edits. </summary>
private void EditButtons()
{
var buttonSize = new Vector2(150 * Im.Style.GlobalScale, 0);
var folderExists = Directory.Exists(_mod.ModPath.FullName);
if (ImEx.Button("Open Mod Directory"u8, buttonSize, folderExists
? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice."
: $"Mod directory \"{_mod.ModPath.FullName}\" does not exist.", !folderExists))
Process.Start(new ProcessStartInfo(_mod.ModPath.FullName) { UseShellExecute = true });
Im.Line.Same();
if (ImEx.Button("Reload Mod"u8, buttonSize, "Reload the current mod from its files.\n"u8
+ "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead."u8,
false))
modManager.ReloadMod(_mod);
BackupButtons(buttonSize);
MoveDirectory.Draw(modManager, _mod, buttonSize);
UiHelpers.DefaultLineSpace();
}
private void BackupButtons(Vector2 buttonSize)
{
var backup = new ModBackup(modExportManager, _mod);
if (ImEx.Button("Export Mod"u8, buttonSize, ModBackup.CreatingBackup
? "Already exporting a mod."
: backup.Exists
? $"Overwrite current exported mod \"{backup.Name}\" with current mod."
: $"Create exported archive of current mod at \"{backup.Name}\".", ModBackup.CreatingBackup))
backup.CreateAsync();
if (Im.Item.RightClicked())
Im.Popup.Open("context"u8);
Im.Line.Same();
if (ImEx.Button("Delete Export"u8, buttonSize, backup.Exists
? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)."
: $"Exported mod \"{backup.Name}\" does not exist.", !backup.Exists || !config.DeleteModModifier.IsActive()))
backup.Delete();
Im.Line.Same();
if (ImEx.Button("Restore From Export"u8, buttonSize, backup.Exists
? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)."
: $"Exported mod \"{backup.Name}\" does not exist.", !backup.Exists || !config.DeleteModModifier.IsActive()))
backup.Restore(modManager);
if (backup.Exists)
{
Im.Line.Same();
ImEx.Icon.Draw(FontAwesomeIcon.CheckCircle.Icon());
Im.Tooltip.OnHover($"Export exists in \"{backup.Name}\".");
}
using var context = Im.Popup.Begin("context"u8);
if (!context)
return;
if (Im.Selectable("Open Backup Directory"u8))
Process.Start(new ProcessStartInfo(modExportManager.ExportDirectory.FullName) { UseShellExecute = true });
}
/// <summary> Anything about editing the regular meta information about the mod. </summary>
private void EditRegularMeta()
{
if (Input.Text("Name"u8, Input.Name, Input.None, _mod.Name, out var newName, UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModName(_mod, newName);
if (Input.Text("Author"u8, Input.Author, Input.None, _mod.Author, out var newAuthor, UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModAuthor(_mod, newAuthor);
if (Input.Text("Version"u8, Input.Version, Input.None, _mod.Version, out var newVersion,
UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModVersion(_mod, newVersion);
if (Input.Text("Website"u8, Input.Website, Input.None, _mod.Website, out var newWebsite,
UiHelpers.InputTextWidth.X))
modManager.DataEditor.ChangeModWebsite(_mod, newWebsite);
using var style = ImStyleDouble.ItemSpacing.Push(new Vector2(Im.Style.GlobalScale * 3));
var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0);
if (Im.Button("Edit Description"u8, reducedSize))
descriptionPopup.Open(_mod);
Im.Line.Same();
var fileExists = File.Exists(filenames.ModMetaPath(_mod));
var tt = fileExists
? "Open the metadata json file in the text editor of your choice."u8
: "The metadata json file does not exist."u8;
using (Im.Id.Push("meta"))
{
if (ImEx.Icon.Button(LunaStyle.FileExportIcon, tt, !fileExists))
Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true });
}
DrawOpenDefaultMod();
}
private void EditLocalData()
{
DrawImportDate();
DrawOpenLocalData();
}
private void DrawImportDate()
{
ImEx.TextFramed($"{DateTimeOffset.FromUnixTimeMilliseconds(_mod.ImportDate).ToLocalTime():yyyy/MM/dd HH:mm}",
new Vector2(UiHelpers.InputTextMinusButton3, 0), ImGuiColor.FrameBackground.Get(0.5f));
Im.Line.Same(0, 3 * Im.Style.GlobalScale);
var canRefresh = config.DeleteModModifier.IsActive();
if (ImEx.Icon.Button(LunaStyle.RefreshIcon, canRefresh
? "Reset the import date to the current date and time."u8
: $"Reset the import date to the current date and time.\nHold {config.DeleteModModifier} while clicking to refresh.",
!canRefresh))
modManager.DataEditor.ResetModImportDate(_mod);
Im.Line.SameInner();
Im.Text("Import Date"u8);
}
private void DrawOpenLocalData()
{
var file = filenames.LocalDataFile(_mod);
var fileExists = File.Exists(file);
var tt = fileExists
? "Open the local mod data file in the text editor of your choice."u8
: "The local mod data file does not exist."u8;
if (ImEx.Button("Open Local Data"u8, UiHelpers.InputTextWidth, tt, !fileExists))
Process.Start(new ProcessStartInfo(file) { UseShellExecute = true });
}
private void DrawOpenDefaultMod()
{
var file = filenames.OptionGroupFile(_mod, -1, false);
var fileExists = File.Exists(file);
var tt = fileExists
? "Open the default mod data file in the text editor of your choice."u8
: "The default mod data file does not exist."u8;
if (ImEx.Button("Open Default Data"u8, UiHelpers.InputTextWidth, tt, !fileExists))
Process.Start(new ProcessStartInfo(file) { UseShellExecute = true });
}
/// <summary> A text input for the new directory name and a button to apply the move. </summary>
private static class MoveDirectory
{
private static string? _currentModDirectory;
private static NewDirectoryState _state = NewDirectoryState.Identical;
public static void Reset()
{
_currentModDirectory = null;
_state = NewDirectoryState.Identical;
}
public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize)
{
Im.Item.SetNextWidth(buttonSize.X * 2 + Im.Style.ItemSpacing.X);
var tmp = _currentModDirectory ?? mod.ModPath.Name;
if (Im.Input.Text("##newModMove"u8, ref tmp))
{
_currentModDirectory = tmp;
_state = modManager.NewDirectoryValid(mod.ModPath.Name, _currentModDirectory, out _);
}
var (disabled, tt) = _state switch
{
NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
NewDirectoryState.ContainsInvalidSymbols => (true,
$"{_currentModDirectory} contains invalid symbols for FFXIV."),
_ => (true, "Unknown error."),
};
Im.Line.Same();
if (ImEx.Button("Rename Mod Directory"u8, buttonSize, tt, disabled) && _currentModDirectory is not null)
{
modManager.MoveModDirectory(mod, _currentModDirectory);
Reset();
}
Im.Line.Same();
if (LunaStyle.DrawAlignedHelpMarker())
Im.Tooltip.Set(
"The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n"u8
+ "This can currently not be used on pre-existing folders and does not support merges or overwriting."u8);
}
}
/// <summary> Handles input text and integers in separate fields without buffers for every single one. </summary>
private static class Input
{
// Special field indices to reuse the same string buffer.
public const int None = -1;
public const int Name = -2;
public const int Author = -3;
public const int Version = -4;
public const int Website = -5;
public const int Path = -6;
// Temporary strings
private static string? _currentEdit;
private static int _currentField = None;
private static int _optionIndex = None;
public static void Reset()
{
_currentEdit = null;
_currentField = None;
_optionIndex = None;
}
public static bool Text(ReadOnlySpan<byte> label, int field, int option, string oldValue, out string value, float width)
{
var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue;
Im.Item.SetNextWidth(width);
if (Im.Input.Text(label, ref tmp))
{
_currentEdit = tmp;
_optionIndex = option;
_currentField = field;
}
if (Im.Item.DeactivatedAfterEdit && _currentEdit is not null)
{
var ret = _currentEdit != oldValue;
value = _currentEdit;
Reset();
return ret;
}
value = string.Empty;
return false;
}
}
}

View file

@ -1,8 +1,8 @@
using Dalamud.Bindings.ImGui;
using ImSharp;
using Luna;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.UI.Classes;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -22,7 +22,7 @@ public class ModPanelSettingsTab(
CommunicatorService communicator,
ModGroupDrawer modGroupDrawer,
Configuration config)
: ITab, Luna.IUiService
: ITab<ModPanelTab>
{
private bool _inherited;
private bool _temporary;
@ -31,8 +31,11 @@ public class ModPanelSettingsTab(
public ReadOnlySpan<byte> Label
=> "Settings"u8;
public ModPanelTab Identifier
=> ModPanelTab.Settings;
public void DrawHeader()
public void PostTabButton()
=> tutorial.OpenTutorial(BasicTutorialSteps.ModOptions);
public void Reset()

View file

@ -1,10 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.UI.AdvancedWindow;
@ -12,138 +7,88 @@ using ImGuiColor = ImSharp.ImGuiColor;
namespace Penumbra.UI.ModsTab;
public class ModPanelTabBar : IUiService
public enum ModPanelTab
{
private enum ModPanelTabType
{
Description,
Settings,
ChangedItems,
Conflicts,
Collections,
Edit,
};
Description,
Settings,
ChangedItems,
Conflicts,
Collections,
Edit,
};
public readonly ModPanelSettingsTab Settings;
public readonly ModPanelDescriptionTab Description;
public readonly ModPanelCollectionsTab Collections;
public readonly ModPanelConflictsTab Conflicts;
public readonly ModPanelChangedItemsTab ChangedItems;
public readonly ModPanelEditTab Edit;
private readonly ModEditWindowFactory _modEditWindowFactory;
private readonly ModManager _modManager;
private readonly TutorialService _tutorial;
public class ModPanelTabBar : TabBar<ModPanelTab>
{
public readonly ModPanelSettingsTab Settings;
public readonly ModPanelEditTab Edit;
private readonly ModManager _modManager;
private readonly TutorialService _tutorial;
public readonly ITab[] Tabs;
private ModPanelTabType _preferredTab = ModPanelTabType.Settings;
private Mod? _lastMod;
private Mod? _lastMod;
public ModPanelTabBar(ModEditWindowFactory modEditWindowFactory, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager,
TutorialService tutorial, ModPanelCollectionsTab collections)
TutorialService tutorial, ModPanelCollectionsTab collections, Logger log)
: base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit)
{
_modEditWindowFactory = modEditWindowFactory;
Settings = settings;
Description = description;
Conflicts = conflicts;
ChangedItems = changedItems;
Edit = edit;
_modManager = modManager;
_tutorial = tutorial;
Collections = collections;
Flags = TabBarFlags.NoTooltip;
Settings = settings;
Edit = edit;
_modManager = modManager;
_tutorial = tutorial;
Buttons.AddButton(new AdvancedEditingButton(this, modEditWindowFactory), 0);
}
Tabs =
[
Settings,
Description,
Conflicts,
ChangedItems,
Collections,
Edit,
];
private sealed class AdvancedEditingButton(ModPanelTabBar parent, ModEditWindowFactory editFactory) : BaseButton
{
public override ReadOnlySpan<byte> Label
=> "Advanced Editing"u8;
public override void OnClick()
{
if (parent._lastMod is { } mod)
editFactory.OpenForMod(mod);
}
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text(
"Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n"u8
+ "\t\t- file redirections\n"u8
+ "\t\t- file swaps\n"u8
+ "\t\t- metadata manipulations\n"u8
+ "\t\t- model materials\n"u8
+ "\t\t- duplicates\n"u8
+ "\t\t- textures"u8);
}
public void Draw(Mod mod)
{
var tabBarHeight = ImGui.GetCursorPosY();
if (_lastMod != mod)
{
_lastMod = mod;
TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(_preferredTab), out _, () => DrawAdvancedEditingButton(mod), Tabs);
}
else
{
TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ReadOnlySpan<byte>.Empty, out var label, () => DrawAdvancedEditingButton(mod),
Tabs);
_preferredTab = ToType(label);
}
var tabBarHeight = Im.Cursor.Y;
_lastMod = mod;
base.Draw();
DrawFavoriteButton(mod, tabBarHeight);
}
private ReadOnlySpan<byte> ToLabel(ModPanelTabType type)
=> type switch
{
ModPanelTabType.Description => Description.Label,
ModPanelTabType.Settings => Settings.Label,
ModPanelTabType.ChangedItems => ChangedItems.Label,
ModPanelTabType.Conflicts => Conflicts.Label,
ModPanelTabType.Collections => Collections.Label,
ModPanelTabType.Edit => Edit.Label,
_ => ReadOnlySpan<byte>.Empty,
};
private ModPanelTabType ToType(ReadOnlySpan<byte> label)
{
if (label == Description.Label)
return ModPanelTabType.Description;
if (label == Settings.Label)
return ModPanelTabType.Settings;
if (label == ChangedItems.Label)
return ModPanelTabType.ChangedItems;
if (label == Conflicts.Label)
return ModPanelTabType.Conflicts;
if (label == Collections.Label)
return ModPanelTabType.Collections;
if (label == Edit.Label)
return ModPanelTabType.Edit;
return 0;
}
private void DrawAdvancedEditingButton(Mod mod)
{
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
{
_modEditWindowFactory.OpenForMod(mod);
}
ImGuiUtil.HoverTooltip(
"Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n"
+ "\t\t- file redirections\n"
+ "\t\t- file swaps\n"
+ "\t\t- metadata manipulations\n"
+ "\t\t- model materials\n"
+ "\t\t- duplicates\n"
+ "\t\t- textures");
}
private void DrawFavoriteButton(Mod mod, float height)
{
var size = ImEx.Icon.CalculateSize(LunaStyle.FavoriteIcon) + Im.Style.FramePadding * 2;
var newPos = new Vector2(ImGui.GetWindowWidth() - size.X - Im.Style.ItemSpacing.X, height);
if (ImGui.GetScrollMaxX() > 0)
newPos.X += ImGui.GetScrollX();
var newPos = new Vector2(Im.Window.Width - size.X - Im.Style.ItemSpacing.X, height);
if (Im.Scroll.MaximumX > 0)
newPos.X += Im.Scroll.X;
var rectUpper = ImGui.GetWindowPos() + newPos;
var color = ImGui.IsMouseHoveringRect(rectUpper, rectUpper + size) ? Im.Style[ImGuiColor.Text] :
mod.Favorite ? LunaStyle.FavoriteColor : Im.Style[ImGuiColor.TextDisabled];
var rectUpper = Im.Window.Position + newPos;
var color = Im.Mouse.IsHoveringRectangle(rectUpper, rectUpper + size) ? Im.Style[ImGuiColor.Text] :
mod.Favorite ? LunaStyle.FavoriteColor : Im.Style[ImGuiColor.TextDisabled];
using var c = ImGuiColor.Text.Push(color)
.Push(ImGuiColor.Button, Vector4.Zero)
.Push(ImGuiColor.ButtonHovered, Vector4.Zero)
.Push(ImGuiColor.ButtonActive, Vector4.Zero);
ImGui.SetCursorPos(newPos);
Im.Cursor.Position = newPos;
if (ImEx.Icon.Button(LunaStyle.FavoriteIcon))
_modManager.DataEditor.ChangeModFavorite(mod, !mod.Favorite);
@ -151,6 +96,6 @@ public class ModPanelTabBar : IUiService
_tutorial.OpenTutorial(BasicTutorialSteps.Favorites);
if (hovered)
ImGui.SetTooltip("Favorite");
Im.Tooltip.Set("Favorite"u8);
}
}

View file

@ -2,7 +2,7 @@ using Dalamud.Bindings.ImGui;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImSharp;
using OtterGui.Widgets;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.GameData.Actors;
@ -16,7 +16,7 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.ResourceWatcher;
public sealed class ResourceWatcher : IDisposable, ITab, Luna.IUiService
public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{
public const int DefaultMaxEntries = 1024;
public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction;
@ -96,6 +96,9 @@ public sealed class ResourceWatcher : IDisposable, ITab, Luna.IUiService
public ReadOnlySpan<byte> Label
=> "Resource Logger"u8;
public TabType Identifier
=> TabType.ResourceWatcher;
public void DrawContent()
{
UpdateRecords();

View file

@ -2,10 +2,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Table;
using OtterGui.Text;
using Penumbra.Enums;
using Penumbra.Interop.Structs;
using Penumbra.String;
@ -54,16 +51,16 @@ internal sealed class ResourceWatcherTable : Table<Record>
=> DrawByteString(item.Path, 280 * Im.Style.GlobalScale);
}
private static unsafe void DrawByteString(CiByteString path, float length)
private static void DrawByteString(CiByteString path, float length)
{
if (path.IsEmpty)
return;
var size = ImUtf8.CalcTextSize(path.Span);
var size = Im.Font.CalculateSize(path.Span);
var clicked = false;
if (size.X <= length)
{
clicked = ImUtf8.Selectable(path.Span);
clicked = Im.Selectable(path.Span);
}
else
{
@ -71,10 +68,11 @@ internal sealed class ResourceWatcherTable : Table<Record>
using (Im.Group())
{
CiByteString shortPath;
if (fileName != -1)
var icon = FontAwesomeIcon.EllipsisH.Icon();
if (fileName is not -1)
{
using var font = ImRaii.PushFont(UiBuilder.IconFont);
clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString());
using var font = AwesomeIcon.Font.Push();
clicked = Im.Selectable(icon.Span);
Im.Line.SameInner();
shortPath = path.Substring(fileName, path.Length - fileName);
}
@ -83,14 +81,14 @@ internal sealed class ResourceWatcherTable : Table<Record>
shortPath = path;
}
clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap);
clicked |= Im.Selectable(shortPath.Span, false, SelectableFlags.AllowOverlap);
}
Im.Tooltip.OnHover(path.Span);
}
if (clicked)
ImUtf8.SetClipboardText(path.Span);
Im.Clipboard.Set(path.Span);
}
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
@ -153,13 +151,13 @@ internal sealed class ResourceWatcherTable : Table<Record>
public override float Width
=> UiBuilder.MonoFont.GetCharAdvance('0') * 17;
public override unsafe string ToName(Record item)
=> item.Crc64 != 0 ? $"{item.Crc64:X16}" : string.Empty;
public override string ToName(Record item)
=> item.Crc64 is not 0 ? $"{item.Crc64:X16}" : string.Empty;
public override unsafe void DrawColumn(Record item, int _)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null);
ImUtf8.Text(ToName(item));
using var font = item.Handle is null ? null : Im.Font.PushMono();
Im.Text(ToName(item));
}
}
@ -334,17 +332,17 @@ internal sealed class ResourceWatcherTable : Table<Record>
var (icon, color, tt) = item.LoadState switch
{
LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(),
$"Successfully loaded ({(byte)item.LoadState})."),
new StringU8($"Successfully loaded ({(byte)item.LoadState}).")),
LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(),
$"Dependencies failed to load ({(byte)item.LoadState})."),
new StringU8($"Dependencies failed to load ({(byte)item.LoadState}).")),
<= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(),
$"Not yet loaded ({(byte)item.LoadState})."),
< LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."),
new StringU8($"Not yet loaded ({(byte)item.LoadState}).")),
< LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), new StringU8($"Loading asynchronously ({(byte)item.LoadState}).")),
> LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(),
$"Failed to load ({(byte)item.LoadState})."),
new StringU8($"Failed to load ({(byte)item.LoadState}).")),
};
ImEx.Icon.Draw(icon.Icon(), color);
ImGuiUtil.HoverTooltip(tt);
Im.Tooltip.OnHover(tt);
}
public override int Compare(Record lhs, Record rhs)
@ -361,8 +359,8 @@ internal sealed class ResourceWatcherTable : Table<Record>
public override unsafe void DrawColumn(Record item, int _)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null);
ImGuiUtil.RightAlign(ToName(item));
using var font = item.Handle is null ? null : Im.Font.PushMono();
ImEx.TextRightAligned(ToName(item));
}
}
@ -447,7 +445,7 @@ internal sealed class ResourceWatcherTable : Table<Record>
=> 30 * Im.Style.GlobalScale;
public override void DrawColumn(Record item, int _)
=> ImGuiUtil.RightAlign(item.RefCount.ToString());
=> ImEx.TextRightAligned($"{item.RefCount}");
public override int Compare(Record lhs, Record rhs)
=> lhs.RefCount.CompareTo(rhs.RefCount);
@ -462,7 +460,7 @@ internal sealed class ResourceWatcherTable : Table<Record>
=> item.OsThreadId.ToString();
public override void DrawColumn(Record item, int _)
=> ImGuiUtil.RightAlign(ToName(item));
=> ImEx.TextRightAligned($"{item.OsThreadId}");
public override int Compare(Record lhs, Record rhs)
=> lhs.OsThreadId.CompareTo(rhs.OsThreadId);

View file

@ -1,9 +1,9 @@
using Dalamud.Bindings.ImGui;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -15,19 +15,22 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public class ChangedItemsTab(
public sealed class ChangedItemsTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator)
: ITab, Luna.IUiService
: ITab<TabType>
{
public ReadOnlySpan<byte> Label
=> "Changed Items"u8;
private string _changedItemFilter = string.Empty;
private string _changedItemModFilter = string.Empty;
private Vector2 _buttonSize;
public TabType Identifier
=> TabType.ChangedItems;
private string _changedItemFilter = string.Empty;
private string _changedItemModFilter = string.Empty;
private Vector2 _buttonSize;
public void DrawContent()
{
@ -105,7 +108,7 @@ public class ChangedItemsTab(
if (ImUtf8.Selectable(first.Name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 })
&& ImGui.GetIO().KeyCtrl
&& first is Mod mod)
communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, mod));
communicator.SelectTab.Invoke(new SelectTab.Arguments(Api.Enums.TabType.Mods, mod));
if (!Im.Item.Hovered())
return;

View file

@ -2,8 +2,9 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin;
using ImSharp;
using Luna;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Mods.Manager;
@ -12,7 +13,7 @@ using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public sealed class CollectionsTab : IDisposable, ITab, Luna.IUiService
public sealed class CollectionsTab : ITab<TabType>, IDisposable
{
private readonly EphemeralConfig _config;
private readonly CollectionSelector _selector;
@ -38,6 +39,9 @@ public sealed class CollectionsTab : IDisposable, ITab, Luna.IUiService
}
}
public TabType Identifier
=> TabType.Collections;
public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito,
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService)
{

View file

@ -1,113 +0,0 @@
using Dalamud.Bindings.ImGui;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Services;
using Penumbra.UI.Tabs.Debug;
using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher;
namespace Penumbra.UI.Tabs;
public class ConfigTabBar : IDisposable, Luna.IUiService
{
private readonly CommunicatorService _communicator;
public readonly SettingsTab Settings;
public readonly ModsTab Mods;
public readonly CollectionsTab Collections;
public readonly ChangedItemsTab ChangedItems;
public readonly EffectiveTab Effective;
public readonly DebugTab Debug;
public readonly ResourceTab Resource;
public readonly Watcher Watcher;
public readonly OnScreenTab OnScreen;
public readonly MessagesTab Messages;
public readonly ITab[] Tabs;
/// <summary> The tab to select on the next Draw call, if any. </summary>
public TabType SelectTab = TabType.None;
public ConfigTabBar(CommunicatorService communicator, SettingsTab settings, ModsTab mods, CollectionsTab collections,
ChangedItemsTab changedItems, EffectiveTab effective, DebugTab debug, ResourceTab resource, Watcher watcher,
OnScreenTab onScreen, MessagesTab messages)
{
_communicator = communicator;
Settings = settings;
Mods = mods;
Collections = collections;
ChangedItems = changedItems;
Effective = effective;
Debug = debug;
Resource = resource;
Watcher = watcher;
OnScreen = onScreen;
Messages = messages;
Tabs =
[
Settings,
Collections,
Mods,
ChangedItems,
Effective,
OnScreen,
Debug,
Resource,
Watcher,
Messages,
];
_communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar);
}
public void Dispose()
=> _communicator.SelectTab.Unsubscribe(OnSelectTab);
public TabType Draw()
{
if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out var currentLabel, () => { }, Tabs))
SelectTab = TabType.None;
return FromLabel(currentLabel);
}
private ReadOnlySpan<byte> ToLabel(TabType type)
=> type switch
{
TabType.Settings => Settings.Label,
TabType.Mods => Mods.Label,
TabType.Collections => Collections.Label,
TabType.ChangedItems => ChangedItems.Label,
TabType.EffectiveChanges => Effective.Label,
TabType.OnScreen => OnScreen.Label,
TabType.ResourceWatcher => Watcher.Label,
TabType.Debug => Debug.Label,
TabType.ResourceManager => Resource.Label,
TabType.Messages => Messages.Label,
_ => ReadOnlySpan<byte>.Empty,
};
private TabType FromLabel(ReadOnlySpan<byte> label)
{
// @formatter:off
if (label == Mods.Label) return TabType.Mods;
if (label == Collections.Label) return TabType.Collections;
if (label == Settings.Label) return TabType.Settings;
if (label == ChangedItems.Label) return TabType.ChangedItems;
if (label == Effective.Label) return TabType.EffectiveChanges;
if (label == OnScreen.Label) return TabType.OnScreen;
if (label == Messages.Label) return TabType.Messages;
if (label == Watcher.Label) return TabType.ResourceWatcher;
if (label == Debug.Label) return TabType.Debug;
if (label == Resource.Label) return TabType.ResourceManager;
// @formatter:on
return TabType.None;
}
private void OnSelectTab(in SelectTab.Arguments arguments)
{
SelectTab = arguments.Tab;
if (arguments.Mod is not null)
Mods.SelectMod = arguments.Mod;
}
}

View file

@ -14,8 +14,8 @@ using Luna;
using Microsoft.Extensions.DependencyInjection;
using OtterGui;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
@ -67,7 +67,7 @@ public class Diagnostics(ServiceManager provider) : IUiService
}
}
public class DebugTab : Window, ITab
public sealed class DebugTab : Window, ITab<TabType>
{
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
@ -173,6 +173,9 @@ public class DebugTab : Window, ITab
public bool IsVisible
=> _config is { DebugMode: true, Ephemeral.DebugSeparateWindow: false };
public TabType Identifier
=> TabType.Debug;
#if DEBUG
private const string DebugVersionString = "(Debug)";
#else

View file

@ -1,10 +1,11 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager;
@ -15,12 +16,15 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
: ITab, Luna.IUiService
public sealed class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
: ITab<TabType>
{
public ReadOnlySpan<byte> Label
=> "Effective Changes"u8;
public TabType Identifier
=> TabType.EffectiveChanges;
public void DrawContent()
{
SetupEffectiveSizes();

View file

@ -0,0 +1,55 @@
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Services;
using Penumbra.UI.Tabs.Debug;
using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher;
namespace Penumbra.UI.Tabs;
public sealed class MainTabBar : TabBar<TabType>, IDisposable
{
public readonly ModsTab Mods;
private readonly EphemeralConfig _config;
private readonly SelectTab _selectTab;
public MainTabBar(Logger log,
SettingsTab settings,
ModsTab mods,
CollectionsTab collections,
ChangedItemsTab changedItems,
EffectiveTab effectiveChanges,
DebugTab debug,
ResourceTab resources,
Watcher watcher,
OnScreenTab onScreen,
MessagesTab messages, EphemeralConfig config, CommunicatorService communicator)
: base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen,
resources, watcher, debug, messages)
{
Mods = mods;
_config = config;
_selectTab = communicator.SelectTab;
_selectTab.Subscribe(OnSelectTab, SelectTab.Priority.MainTabBar);
TabSelected.Subscribe(OnTabSelected, 0);
}
private void OnSelectTab(in SelectTab.Arguments arguments)
{
NextTab = arguments.Tab;
if (arguments.Mod is not null)
Mods.SelectMod = arguments.Mod;
}
public void Dispose()
{
_selectTab.Unsubscribe(OnSelectTab);
}
private void OnTabSelected(in TabType type)
{
_config.SelectedTab = type;
_config.Save();
}
}

View file

@ -1,9 +1,10 @@
using OtterGui.Widgets;
using Penumbra.Services;
using Luna;
using Penumbra.Api.Enums;
using MessageService = Penumbra.Services.MessageService;
namespace Penumbra.UI.Tabs;
public class MessagesTab(MessageService messages) : ITab, Luna.IUiService
public sealed class MessagesTab(MessageService messages) : ITab<TabType>
{
public ReadOnlySpan<byte> Label
=> "Messages"u8;
@ -13,4 +14,7 @@ public class MessagesTab(MessageService messages) : ITab, Luna.IUiService
public void DrawContent()
=> messages.DrawNotificationLog();
public TabType Identifier
=> TabType.Messages;
}

View file

@ -7,7 +7,7 @@ using Dalamud.Interface;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using ImSharp;
using OtterGui.Widgets;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Interop.Services;
using Penumbra.Mods;
@ -19,7 +19,7 @@ using Penumbra.GameData.Interop;
namespace Penumbra.UI.Tabs;
public class ModsTab(
public sealed class ModsTab(
ModManager modManager,
CollectionManager collectionManager,
ModFileSystemSelector selector,
@ -31,7 +31,7 @@ public class ModsTab(
CollectionSelectHeader collectionHeader,
ITargetManager targets,
ObjectManager objects)
: ITab, Luna.IUiService
: ITab<TabType>
{
private readonly ActiveCollections _activeCollections = collectionManager.Active;
@ -41,7 +41,10 @@ public class ModsTab(
public ReadOnlySpan<byte> Label
=> "Mods"u8;
public void DrawHeader()
public TabType Identifier
=> TabType.Mods;
public void PostTabButton()
=> tutorial.OpenTutorial(BasicTutorialSteps.Mods);
public Mod SelectMod
@ -60,7 +63,8 @@ public class ModsTab(
collectionHeader.Draw(false);
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
using (var child = ImRaii.Child("##ModsTabMod", Im.ContentRegion.Available with { Y = config.HideRedrawBar ? 0 : -Im.Style.FrameHeight },
using (var child = ImRaii.Child("##ModsTabMod",
Im.ContentRegion.Available with { Y = config.HideRedrawBar ? 0 : -Im.Style.FrameHeight },
true, ImGuiWindowFlags.HorizontalScrollbar))
{
style.Pop();

View file

@ -1,9 +1,10 @@
using OtterGui.Widgets;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.Tabs;
public class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab, Luna.IUiService
public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab<TabType>
{
private readonly ResourceTreeViewer _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { });
@ -12,4 +13,7 @@ public class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) :
public void DrawContent()
=> _viewer.Draw();
public TabType Identifier
=> TabType.OnScreen;
}

View file

@ -4,17 +4,21 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.STD;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.String.Classes;
namespace Penumbra.UI.Tabs;
public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
: ITab, Luna.IUiService
public sealed class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
: ITab<TabType>
{
public TabType Identifier
=> TabType.ResourceManager;
public ReadOnlySpan<byte> Label
=> "Resource Manager"u8;
@ -52,7 +56,8 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa
private string _resourceManagerFilter = string.Empty;
/// <summary> Draw a single resource map. </summary>
private unsafe void DrawResourceMap(ResourceCategory category, uint ext, StdMap<uint, FFXIVClientStructs.Interop.Pointer<ResourceHandle>>* map)
private unsafe void DrawResourceMap(ResourceCategory category, uint ext,
StdMap<uint, FFXIVClientStructs.Interop.Pointer<ResourceHandle>>* map)
{
if (map == null)
return;

View file

@ -1,7 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -12,6 +11,7 @@ using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop;
using Penumbra.Interop.Hooks.PostProcessing;
@ -23,10 +23,13 @@ using Penumbra.UI.ModsTab;
namespace Penumbra.UI.Tabs;
public class SettingsTab : ITab, IUiService
public sealed class SettingsTab : ITab<TabType>
{
public const int RootDirectoryMaxLength = 64;
public TabType Identifier
=> TabType.Settings;
public ReadOnlySpan<byte> Label
=> "Settings"u8;