mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
615 lines
No EOL
22 KiB
C#
615 lines
No EOL
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Numerics;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Logging;
|
|
using ImGuiNET;
|
|
using Penumbra.Mod;
|
|
using Penumbra.Mods;
|
|
using Penumbra.UI.Custom;
|
|
using Penumbra.Util;
|
|
|
|
namespace Penumbra.UI;
|
|
|
|
public partial class SettingsInterface
|
|
{
|
|
private class ModPanel
|
|
{
|
|
private const string LabelModPanel = "selectedModInfo";
|
|
private const string LabelEditName = "##editName";
|
|
private const string LabelEditVersion = "##editVersion";
|
|
private const string LabelEditAuthor = "##editAuthor";
|
|
private const string LabelEditWebsite = "##editWebsite";
|
|
private const string LabelModEnabled = "Enabled";
|
|
private const string LabelEditingEnabled = "Enable Editing";
|
|
private const string LabelOverWriteDir = "OverwriteDir";
|
|
private const string ButtonOpenWebsite = "Open Website";
|
|
private const string ButtonOpenModFolder = "Open Mod Folder";
|
|
private const string ButtonRenameModFolder = "Rename Mod Folder";
|
|
private const string ButtonEditJson = "Edit JSON";
|
|
private const string ButtonReloadJson = "Reload JSON";
|
|
private const string ButtonDeduplicate = "Deduplicate";
|
|
private const string ButtonNormalize = "Normalize";
|
|
private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer.";
|
|
private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application.";
|
|
private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json.";
|
|
private const string TooltipReloadJson = "Reload the configuration of all mods.";
|
|
private const string PopupRenameFolder = "Rename Folder";
|
|
|
|
private const string TooltipDeduplicate =
|
|
"Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n"
|
|
+ "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!";
|
|
|
|
private const string TooltipNormalize =
|
|
"Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!";
|
|
|
|
private const float HeaderLineDistance = 10f;
|
|
private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f);
|
|
|
|
private readonly SettingsInterface _base;
|
|
private readonly Selector _selector;
|
|
private readonly HashSet< string > _newMods;
|
|
public readonly PluginDetails Details;
|
|
|
|
private bool _editMode;
|
|
private string _currentWebsite;
|
|
private bool _validWebsite;
|
|
|
|
private string _fromMaterial = string.Empty;
|
|
private string _toMaterial = string.Empty;
|
|
|
|
public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods )
|
|
{
|
|
_base = ui;
|
|
_selector = s;
|
|
_newMods = newMods;
|
|
Details = new PluginDetails( _base, _selector );
|
|
_currentWebsite = Meta?.Website ?? "";
|
|
}
|
|
|
|
private Mod.FullMod? Mod
|
|
=> _selector.Mod;
|
|
|
|
private ModMeta? Meta
|
|
=> Mod?.Data.Meta;
|
|
|
|
private void DrawName()
|
|
{
|
|
var name = Meta!.Name;
|
|
var modManager = Penumbra.ModManager;
|
|
if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) )
|
|
{
|
|
_selector.SelectModOnUpdate( Mod.Data.BasePath.Name );
|
|
if( !modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) )
|
|
{
|
|
Mod.Data.Rename( name );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawVersion()
|
|
{
|
|
if( _editMode )
|
|
{
|
|
ImGui.BeginGroup();
|
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup );
|
|
ImGui.Text( "(Version " );
|
|
|
|
using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector );
|
|
ImGui.SameLine();
|
|
var version = Meta!.Version;
|
|
if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 )
|
|
&& version != Meta.Version )
|
|
{
|
|
Meta.Version = version;
|
|
_selector.SaveCurrentMod();
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.Text( ")" );
|
|
}
|
|
else if( Meta!.Version.Length > 0 )
|
|
{
|
|
ImGui.Text( $"(Version {Meta.Version})" );
|
|
}
|
|
}
|
|
|
|
private void DrawAuthor()
|
|
{
|
|
ImGui.BeginGroup();
|
|
ImGui.TextColored( GreyColor, "by" );
|
|
|
|
ImGui.SameLine();
|
|
var author = Meta!.Author;
|
|
if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 )
|
|
&& author != Meta.Author )
|
|
{
|
|
Meta.Author = author;
|
|
_selector.SaveCurrentMod();
|
|
_selector.Cache.TriggerFilterReset();
|
|
}
|
|
|
|
ImGui.EndGroup();
|
|
}
|
|
|
|
private void DrawWebsite()
|
|
{
|
|
ImGui.BeginGroup();
|
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup );
|
|
if( _editMode )
|
|
{
|
|
ImGui.TextColored( GreyColor, "from" );
|
|
ImGui.SameLine();
|
|
var website = Meta!.Website;
|
|
if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 )
|
|
&& website != Meta.Website )
|
|
{
|
|
Meta.Website = website;
|
|
_selector.SaveCurrentMod();
|
|
}
|
|
}
|
|
else if( Meta!.Website.Length > 0 )
|
|
{
|
|
if( _currentWebsite != Meta.Website )
|
|
{
|
|
_currentWebsite = Meta.Website;
|
|
_validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult )
|
|
&& ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp );
|
|
}
|
|
|
|
if( _validWebsite )
|
|
{
|
|
if( ImGui.SmallButton( ButtonOpenWebsite ) )
|
|
{
|
|
try
|
|
{
|
|
var process = new ProcessStartInfo( Meta.Website )
|
|
{
|
|
UseShellExecute = true,
|
|
};
|
|
Process.Start( process );
|
|
}
|
|
catch( System.ComponentModel.Win32Exception )
|
|
{
|
|
// Do nothing.
|
|
}
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( Meta.Website );
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextColored( GreyColor, "from" );
|
|
ImGui.SameLine();
|
|
ImGui.Text( Meta.Website );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawHeaderLine()
|
|
{
|
|
DrawName();
|
|
ImGui.SameLine();
|
|
DrawVersion();
|
|
ImGui.SameLine();
|
|
DrawAuthor();
|
|
ImGui.SameLine();
|
|
DrawWebsite();
|
|
}
|
|
|
|
private void DrawPriority()
|
|
{
|
|
var priority = Mod!.Settings.Priority;
|
|
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
|
|
if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority )
|
|
{
|
|
Penumbra.CollectionManager.Current.SetModPriority( Mod.Data.Index, priority );
|
|
_selector.Cache.TriggerFilterReset();
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip(
|
|
"Higher priority mods take precedence over other mods in the case of file conflicts.\n"
|
|
+ "In case of identical priority, the alphabetically first mod takes precedence." );
|
|
}
|
|
|
|
private void DrawEnabledMark()
|
|
{
|
|
var enabled = Mod!.Settings.Enabled;
|
|
if( ImGui.Checkbox( LabelModEnabled, ref enabled ) )
|
|
{
|
|
Penumbra.CollectionManager.Current.SetModState( Mod.Data.Index, enabled );
|
|
if( enabled )
|
|
{
|
|
_newMods.Remove( Mod.Data.BasePath.Name );
|
|
}
|
|
_selector.Cache.TriggerFilterReset();
|
|
}
|
|
}
|
|
|
|
public static bool DrawSortOrder( Mod.Mod mod, Mod.Mod.Manager manager, Selector selector )
|
|
{
|
|
var currentSortOrder = mod.Order.FullPath;
|
|
ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale );
|
|
if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
|
{
|
|
manager.ChangeSortOrder( mod, currentSortOrder );
|
|
selector.SelectModOnUpdate( mod.BasePath.Name );
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void DrawEditableMark()
|
|
{
|
|
ImGui.Checkbox( LabelEditingEnabled, ref _editMode );
|
|
}
|
|
|
|
private void DrawOpenModFolderButton()
|
|
{
|
|
Mod!.Data.BasePath.Refresh();
|
|
if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists )
|
|
{
|
|
Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipOpenModFolder );
|
|
}
|
|
|
|
private string _newName = "";
|
|
private bool _keyboardFocus = true;
|
|
|
|
private void RenameModFolder( string newName )
|
|
{
|
|
_newName = newName.ReplaceBadXivSymbols();
|
|
if( _newName.Length == 0 )
|
|
{
|
|
PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName );
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) )
|
|
{
|
|
var dir = Mod!.Data.BasePath;
|
|
DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName ));
|
|
|
|
if( newDir.Exists )
|
|
{
|
|
ImGui.OpenPopup( LabelOverWriteDir );
|
|
}
|
|
else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) )
|
|
{
|
|
_selector.ReloadCurrentMod();
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
}
|
|
else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCulture ) )
|
|
{
|
|
var dir = Mod!.Data.BasePath;
|
|
DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName ));
|
|
var sourceUri = new Uri( dir.FullName );
|
|
var targetUri = new Uri( newDir.FullName );
|
|
if( sourceUri.Equals( targetUri ) )
|
|
{
|
|
var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName );
|
|
if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) )
|
|
{
|
|
if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) )
|
|
{
|
|
PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." );
|
|
Penumbra.ModManager.RenameModFolder( Mod.Data, dir );
|
|
}
|
|
|
|
_selector.ReloadCurrentMod();
|
|
}
|
|
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
else
|
|
{
|
|
ImGui.OpenPopup( LabelOverWriteDir );
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target )
|
|
{
|
|
try
|
|
{
|
|
foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) )
|
|
{
|
|
var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) );
|
|
if( targetFile.Exists )
|
|
{
|
|
targetFile.Delete();
|
|
}
|
|
|
|
targetFile.Directory?.Create();
|
|
file.MoveTo( targetFile.FullName );
|
|
}
|
|
|
|
source.Delete( true );
|
|
return true;
|
|
}
|
|
catch( Exception e )
|
|
{
|
|
PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool OverwriteDirPopup()
|
|
{
|
|
var closeParent = false;
|
|
var _ = true;
|
|
if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) )
|
|
{
|
|
return closeParent;
|
|
}
|
|
|
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
|
|
|
var dir = Mod!.Data.BasePath;
|
|
DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName ));
|
|
ImGui.Text(
|
|
$"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." );
|
|
var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 );
|
|
if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) )
|
|
{
|
|
Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false );
|
|
|
|
_selector.SelectModOnUpdate( _newName );
|
|
|
|
closeParent = true;
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
|
|
if( ImGui.Button( "Cancel", buttonSize ) )
|
|
{
|
|
_keyboardFocus = true;
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
|
|
return closeParent;
|
|
}
|
|
|
|
private void DrawRenameModFolderPopup()
|
|
{
|
|
var _ = true;
|
|
_keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder );
|
|
|
|
ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) );
|
|
if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
|
|
|
if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
|
|
{
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
|
|
var newName = Mod!.Data.BasePath.Name;
|
|
|
|
if( _keyboardFocus )
|
|
{
|
|
ImGui.SetKeyboardFocusHere();
|
|
_keyboardFocus = false;
|
|
}
|
|
|
|
if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
|
{
|
|
RenameModFolder( newName );
|
|
}
|
|
|
|
ImGui.TextColored( GreyColor,
|
|
"Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." );
|
|
|
|
ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 );
|
|
|
|
|
|
if( OverwriteDirPopup() )
|
|
{
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
}
|
|
|
|
private void DrawRenameModFolderButton()
|
|
{
|
|
DrawRenameModFolderPopup();
|
|
if( ImGui.Button( ButtonRenameModFolder ) )
|
|
{
|
|
ImGui.OpenPopup( PopupRenameFolder );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipRenameModFolder );
|
|
}
|
|
|
|
private void DrawEditJsonButton()
|
|
{
|
|
if( ImGui.Button( ButtonEditJson ) )
|
|
{
|
|
_selector.SaveCurrentMod();
|
|
Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipEditJson );
|
|
}
|
|
|
|
private void DrawReloadJsonButton()
|
|
{
|
|
if( ImGui.Button( ButtonReloadJson ) )
|
|
{
|
|
_selector.ReloadCurrentMod( true, false );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipReloadJson );
|
|
}
|
|
|
|
private void DrawResetMetaButton()
|
|
{
|
|
if( ImGui.Button( "Recompute Metadata" ) )
|
|
{
|
|
_selector.ReloadCurrentMod( true, true, true );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip(
|
|
"Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n"
|
|
+ "Also reloads the mod.\n"
|
|
+ "Be aware that this removes all manually added metadata changes." );
|
|
}
|
|
|
|
private void DrawDeduplicateButton()
|
|
{
|
|
if( ImGui.Button( ButtonDeduplicate ) )
|
|
{
|
|
ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! );
|
|
_selector.SaveCurrentMod();
|
|
_selector.ReloadCurrentMod( true, true, true );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipDeduplicate );
|
|
}
|
|
|
|
private void DrawNormalizeButton()
|
|
{
|
|
if( ImGui.Button( ButtonNormalize ) )
|
|
{
|
|
ModCleanup.Normalize( Mod!.Data.BasePath, Meta! );
|
|
_selector.SaveCurrentMod();
|
|
_selector.ReloadCurrentMod( true, true, true );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( TooltipNormalize );
|
|
}
|
|
|
|
private void DrawAutoGenerateGroupsButton()
|
|
{
|
|
if( ImGui.Button( "Auto-Generate Groups" ) )
|
|
{
|
|
ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! );
|
|
_selector.SaveCurrentMod();
|
|
_selector.ReloadCurrentMod( true, true );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n"
|
|
+ "First subdirectory: Option Group\n"
|
|
+ "Second subdirectory: Option Name\n"
|
|
+ "Afterwards: Relative file paths.\n"
|
|
+ "Experimental - Use at own risk!" );
|
|
}
|
|
|
|
private void DrawSplitButton()
|
|
{
|
|
if( ImGui.Button( "Split Mod" ) )
|
|
{
|
|
ModCleanup.SplitMod( Mod!.Data );
|
|
}
|
|
|
|
ImGuiCustom.HoverTooltip(
|
|
"Split off all options of a mod into single mods that are placed in a collective folder.\n"
|
|
+ "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n"
|
|
+ "Experimental - Use at own risk!" );
|
|
}
|
|
|
|
private void DrawMaterialChangeRow()
|
|
{
|
|
ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale );
|
|
ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 );
|
|
ImGui.SameLine();
|
|
using var font = ImGuiRaii.PushFont( UiBuilder.IconFont );
|
|
ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() );
|
|
font.Pop();
|
|
ImGui.SameLine();
|
|
ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale );
|
|
ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 );
|
|
ImGui.SameLine();
|
|
var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial );
|
|
using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings );
|
|
if( ImGui.Button( "Convert" ) && validStrings )
|
|
{
|
|
ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial );
|
|
}
|
|
|
|
alpha.Pop();
|
|
|
|
ImGuiCustom.HoverTooltip(
|
|
"Change the skin material of all models in this mod reference "
|
|
+ "from the suffix given in the first text input to "
|
|
+ "the suffix given in the second input.\n"
|
|
+ "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n"
|
|
+ "This overwrites .mdl files, use at your own risk!" );
|
|
}
|
|
|
|
private void DrawEditLine()
|
|
{
|
|
DrawOpenModFolderButton();
|
|
ImGui.SameLine();
|
|
DrawRenameModFolderButton();
|
|
ImGui.SameLine();
|
|
DrawEditJsonButton();
|
|
ImGui.SameLine();
|
|
DrawReloadJsonButton();
|
|
|
|
DrawResetMetaButton();
|
|
ImGui.SameLine();
|
|
DrawDeduplicateButton();
|
|
ImGui.SameLine();
|
|
DrawNormalizeButton();
|
|
ImGui.SameLine();
|
|
DrawAutoGenerateGroupsButton();
|
|
ImGui.SameLine();
|
|
DrawSplitButton();
|
|
|
|
DrawMaterialChangeRow();
|
|
|
|
DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector );
|
|
}
|
|
|
|
public void Draw()
|
|
{
|
|
try
|
|
{
|
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild );
|
|
var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true );
|
|
|
|
if( !ret || Mod == null )
|
|
{
|
|
return;
|
|
}
|
|
|
|
DrawHeaderLine();
|
|
|
|
// Next line with fixed distance.
|
|
ImGuiCustom.VerticalDistance( HeaderLineDistance );
|
|
|
|
DrawEnabledMark();
|
|
ImGui.SameLine();
|
|
DrawPriority();
|
|
if( Penumbra.Config.ShowAdvanced )
|
|
{
|
|
ImGui.SameLine();
|
|
DrawEditableMark();
|
|
}
|
|
|
|
// Next line, if editable.
|
|
if( _editMode )
|
|
{
|
|
DrawEditLine();
|
|
}
|
|
|
|
Details.Draw( _editMode );
|
|
}
|
|
catch( Exception ex )
|
|
{
|
|
PluginLog.LogError( ex, "Oh no" );
|
|
}
|
|
}
|
|
}
|
|
} |