602 lines
25 KiB
C#
Executable File
602 lines
25 KiB
C#
Executable File
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using HellionChat.Code;
|
|
using HellionChat.Resources;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui.SettingsTabs;
|
|
|
|
internal sealed class Tabs : ISettingsTab
|
|
{
|
|
private Plugin Plugin { get; }
|
|
private Configuration Mutable { get; }
|
|
|
|
public string Name => HellionStrings.Settings_Tab_Tabs + "###tabs-tabs";
|
|
|
|
private int ToOpen = -2;
|
|
|
|
internal Tabs(Plugin plugin, Configuration mutable)
|
|
{
|
|
Plugin = plugin;
|
|
Mutable = mutable;
|
|
}
|
|
|
|
public void Draw(bool sectionJustEntered)
|
|
{
|
|
const string addTabPopup = "add-tab-popup";
|
|
|
|
ImGuiUtil.HelpText(HellionStrings.Tabs_Presets_Linkshell_Hint);
|
|
ImGui.Spacing();
|
|
|
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.Plus, tooltip: Language.Options_Tabs_Add))
|
|
ImGui.OpenPopup(addTabPopup);
|
|
|
|
using (var popup = ImRaii.Popup(addTabPopup))
|
|
{
|
|
if (popup)
|
|
{
|
|
if (ImGui.Selectable(Language.Options_Tabs_NewTab))
|
|
Mutable.Tabs.Add(new Tab());
|
|
|
|
ImGui.Separator();
|
|
|
|
if (
|
|
ImGui.Selectable(
|
|
string.Format(Language.Options_Tabs_Preset, Language.Tabs_Presets_General)
|
|
)
|
|
)
|
|
Mutable.Tabs.Add(TabsUtil.VanillaGeneral);
|
|
|
|
if (
|
|
ImGui.Selectable(
|
|
string.Format(Language.Options_Tabs_Preset, Language.Tabs_Presets_Event)
|
|
)
|
|
)
|
|
Mutable.Tabs.Add(TabsUtil.VanillaEvent);
|
|
|
|
if (
|
|
ImGui.Selectable(
|
|
string.Format(Language.Options_Tabs_Preset, Language.Tabs_Presets_Tell)
|
|
)
|
|
)
|
|
Mutable.Tabs.Add(TabsUtil.VanillaTellExclusive);
|
|
}
|
|
}
|
|
|
|
var toRemove = -1;
|
|
var doOpens = ToOpen > -2;
|
|
for (var i = 0; i < Mutable.Tabs.Count; i++)
|
|
{
|
|
var tab = Mutable.Tabs[i];
|
|
|
|
// Sub-sections (Channels/Display/Notification/Input/Pop-out) are inlined into
|
|
// this loop body rather than extracted to helpers, because each one closes over
|
|
// the per-iteration `i` and `tab` state. Extraction would mean passing both
|
|
// into every helper without meaningful encapsulation gain.
|
|
|
|
// ToOpen controls which tab-item TreeNode is open (e.g. after add/move).
|
|
// This is the outer level — not touched by sectionJustEntered.
|
|
if (doOpens)
|
|
ImGui.SetNextItemOpen(i == ToOpen);
|
|
|
|
using var treeNode = ImRaii.TreeNode($"{tab.Name}###tab-{i}");
|
|
if (!treeNode.Success)
|
|
continue;
|
|
|
|
using var pushedId = ImRaii.PushId($"tab-{i}");
|
|
|
|
if (
|
|
ImGuiUtil.IconButton(
|
|
FontAwesomeIcon.TrashAlt,
|
|
tooltip: Language.Options_Tabs_Delete
|
|
)
|
|
)
|
|
{
|
|
toRemove = i;
|
|
ToOpen = -1;
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (
|
|
ImGuiUtil.IconButton(FontAwesomeIcon.ArrowUp, tooltip: Language.Options_Tabs_MoveUp)
|
|
&& i > 0
|
|
)
|
|
{
|
|
(Mutable.Tabs[i - 1], Mutable.Tabs[i]) = (Mutable.Tabs[i], Mutable.Tabs[i - 1]);
|
|
ToOpen = i - 1;
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (
|
|
ImGuiUtil.IconButton(
|
|
FontAwesomeIcon.ArrowDown,
|
|
tooltip: Language.Options_Tabs_MoveDown
|
|
)
|
|
&& i < Mutable.Tabs.Count - 1
|
|
)
|
|
{
|
|
(Mutable.Tabs[i + 1], Mutable.Tabs[i]) = (Mutable.Tabs[i], Mutable.Tabs[i + 1]);
|
|
ToOpen = i + 1;
|
|
}
|
|
|
|
// Name and Icon are always visible — no sub-section collapse for these.
|
|
ImGui.InputText(
|
|
Language.Options_Tabs_Name,
|
|
ref tab.Name,
|
|
512,
|
|
ImGuiInputTextFlags.EnterReturnsTrue
|
|
);
|
|
|
|
// Per-tab icon override added in v1.2.0. Falls back to default mapping if unset.
|
|
ImGui.TextUnformatted(HellionStrings.Tabs_Icon_Label);
|
|
ImGui.SameLine();
|
|
ImGuiUtil.HelpMarker(HellionStrings.Tabs_Icon_HelpMarker);
|
|
|
|
var iconCurrent = string.IsNullOrEmpty(tab.Icon) ? "" : tab.Icon;
|
|
var iconPreview =
|
|
iconCurrent.Length == 0 ? HellionStrings.Tabs_Icon_DefaultOption : iconCurrent;
|
|
using (var combo = ImRaii.Combo($"##icon-{i}", iconPreview))
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
// First option clears the icon and lets the default mapping take over.
|
|
if (
|
|
ImGui.Selectable(
|
|
HellionStrings.Tabs_Icon_DefaultOption,
|
|
iconCurrent.Length == 0
|
|
)
|
|
)
|
|
{
|
|
tab.Icon = null;
|
|
}
|
|
|
|
ImGui.Separator();
|
|
|
|
// Options sourced from TabIconGlyphResolver.PickerOptions (single source of truth).
|
|
foreach (var option in TabIconGlyphResolver.PickerOptions)
|
|
{
|
|
var isSelected = string.Equals(
|
|
iconCurrent,
|
|
option,
|
|
StringComparison.OrdinalIgnoreCase
|
|
);
|
|
if (ImGui.Selectable(option, isSelected))
|
|
{
|
|
tab.Icon = option;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// ── Sub-section: Channels ─────────────────────────────────────────
|
|
// First because it answers "what does this tab collect?" — most important.
|
|
if (sectionJustEntered)
|
|
ImGui.SetNextItemOpen(false);
|
|
using (
|
|
var secChannels = ImRaii.TreeNode(
|
|
HellionStrings.Settings_Section_Tab_Channels + $"##sec-channels-{i}"
|
|
)
|
|
)
|
|
{
|
|
if (secChannels.Success)
|
|
{
|
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, tab.SelectedChannels);
|
|
ImGuiUtil.ExtraChatSelector(
|
|
Language.Options_Tabs_ExtraChatChannels,
|
|
ref tab.ExtraChatAll,
|
|
tab.ExtraChatChannels
|
|
);
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// ── Sub-section: Display ──────────────────────────────────────────
|
|
if (sectionJustEntered)
|
|
ImGui.SetNextItemOpen(false);
|
|
using (
|
|
var secDisplay = ImRaii.TreeNode(
|
|
HellionStrings.Settings_Section_Tab_Display + $"##sec-display-{i}"
|
|
)
|
|
)
|
|
{
|
|
if (secDisplay.Success)
|
|
{
|
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
|
|
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
|
|
|
|
using (
|
|
var combo = ImGuiUtil.BeginComboVertical(
|
|
Language.Options_Tabs_UnreadMode,
|
|
tab.UnreadMode.Name()
|
|
)
|
|
)
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
foreach (var mode in Enum.GetValues<UnreadMode>())
|
|
{
|
|
if (ImGui.Selectable(mode.Name(), tab.UnreadMode == mode))
|
|
tab.UnreadMode = mode;
|
|
|
|
if (mode.Tooltip() is { } tooltip && ImGui.IsItemHovered())
|
|
ImGuiUtil.Tooltip(tooltip);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only relevant when the global hide-when-inactive is on.
|
|
if (Mutable.HideWhenInactive)
|
|
ImGui.Checkbox(
|
|
Language.Options_Tabs_InactivityBehaviour,
|
|
ref tab.UnhideOnActivity
|
|
);
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// ── Sub-section: Notification ─────────────────────────────────────
|
|
if (sectionJustEntered)
|
|
ImGui.SetNextItemOpen(false);
|
|
using (
|
|
var secNotif = ImRaii.TreeNode(
|
|
HellionStrings.Settings_Section_Tab_Notification + $"##sec-notif-{i}"
|
|
)
|
|
)
|
|
{
|
|
if (secNotif.Success)
|
|
{
|
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
|
|
ImGui.Checkbox(
|
|
HellionStrings.Tabs_NotificationSound_Enable_Name,
|
|
ref tab.EnableNotificationSound
|
|
);
|
|
ImGuiUtil.HelpMarker(HellionStrings.Tabs_NotificationSound_Description);
|
|
if (tab.EnableNotificationSound)
|
|
{
|
|
using var notifIndent = ImRaii.PushIndent(10.0f);
|
|
// Build a readable preview label for the currently selected sound.
|
|
var soundPreview =
|
|
tab.NotificationSoundId <= 16
|
|
? $"{HellionStrings.Tabs_NotificationSound_Option} {tab.NotificationSoundId}"
|
|
: $"{HellionStrings.Tabs_NotificationSound_CustomOption} {tab.NotificationSoundId - 16}";
|
|
using (var combo = ImRaii.Combo($"##notif-sound-{i}", soundPreview))
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
for (uint s = 1; s <= 16; s++)
|
|
{
|
|
if (
|
|
ImGui.Selectable(
|
|
$"{HellionStrings.Tabs_NotificationSound_Option} {s}",
|
|
tab.NotificationSoundId == s
|
|
)
|
|
)
|
|
tab.NotificationSoundId = s;
|
|
}
|
|
|
|
ImGui.Separator();
|
|
|
|
// Bundled custom sounds (ids 17-19).
|
|
for (uint n = 1; n <= 3; n++)
|
|
{
|
|
var customId = 16 + n;
|
|
if (
|
|
ImGui.Selectable(
|
|
$"{HellionStrings.Tabs_NotificationSound_CustomOption} {n}",
|
|
tab.NotificationSoundId == customId
|
|
)
|
|
)
|
|
tab.NotificationSoundId = customId;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Let the user hear the currently selected sound without waiting
|
|
// for a real message to arrive in this tab.
|
|
ImGui.SameLine();
|
|
if (
|
|
ImGuiUtil.IconButton(
|
|
FontAwesomeIcon.Play,
|
|
tooltip: HellionStrings.Tabs_NotificationSound_Preview
|
|
)
|
|
)
|
|
{
|
|
var previewId = tab.NotificationSoundId;
|
|
if (previewId <= 16)
|
|
{
|
|
Plugin.Framework.RunOnFrameworkThread(() =>
|
|
{
|
|
unsafe
|
|
{
|
|
UIGlobals.PlaySoundEffect(previewId);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Plugin.CustomAudioPlayer.Play(
|
|
(int)previewId - 16,
|
|
Mutable.CustomSoundVolume
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Volume is stored as a 0-1 float but shown as 0-100%.
|
|
// Same field as General → Sound; shown here for convenience.
|
|
// DragFloatVertical derives its widget ID from the label text and exposes no
|
|
// override. We inline the equivalent (text label + SetNextItemWidth + DragFloat)
|
|
// to keep an explicit ##tab-volume-{i} ID, which reads more clearly than relying
|
|
// on the surrounding PushId("tab-{i}") scope to disambiguate identical labels.
|
|
// Volume is global (Mutable.CustomSoundVolume) and applies to every tab's
|
|
// notification sound, so it is shown unconditionally — not gated by the
|
|
// per-tab EnableNotificationSound toggle.
|
|
ImGui.TextUnformatted(HellionStrings.Settings_General_CustomSoundVolume_Name);
|
|
ImGui.SetNextItemWidth(-1);
|
|
var customSoundVolumePercent = Mutable.CustomSoundVolume * 100f;
|
|
if (
|
|
ImGui.DragFloat(
|
|
$"##tab-volume-{i}",
|
|
ref customSoundVolumePercent,
|
|
1f,
|
|
0f,
|
|
100f,
|
|
$"{customSoundVolumePercent:N0}%%",
|
|
ImGuiSliderFlags.AlwaysClamp
|
|
)
|
|
)
|
|
{
|
|
Mutable.CustomSoundVolume = customSoundVolumePercent / 100f;
|
|
}
|
|
// Applies globally — same value as in General → Sound.
|
|
ImGuiUtil.HelpMarker(
|
|
HellionStrings.Settings_General_CustomSoundVolume_Description
|
|
+ "\n\n"
|
|
+ HellionStrings.Settings_Section_Tab_Volume_AllTabsHint
|
|
);
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// ── Sub-section: Input ────────────────────────────────────────────
|
|
if (sectionJustEntered)
|
|
ImGui.SetNextItemOpen(false);
|
|
using (
|
|
var secInput = ImRaii.TreeNode(
|
|
HellionStrings.Settings_Section_Tab_Input + $"##sec-input-{i}"
|
|
)
|
|
)
|
|
{
|
|
if (secInput.Success)
|
|
{
|
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
|
|
ImGui.Checkbox(Language.Options_Tabs_NoInput, ref tab.InputDisabled);
|
|
if (!tab.InputDisabled)
|
|
{
|
|
var input =
|
|
tab.Channel?.ToChatType().Name()
|
|
?? Language.Options_Tabs_NoInputChannel;
|
|
using (
|
|
var combo = ImGuiUtil.BeginComboVertical(
|
|
Language.Options_Tabs_InputChannel,
|
|
input
|
|
)
|
|
)
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
if (
|
|
ImGui.Selectable(
|
|
Language.Options_Tabs_NoInputChannel,
|
|
tab.Channel == null
|
|
)
|
|
)
|
|
tab.Channel = null;
|
|
|
|
foreach (var channel in Enum.GetValues<InputChannel>())
|
|
if (
|
|
ImGui.Selectable(
|
|
channel.ToChatType().Name(),
|
|
tab.Channel == channel
|
|
)
|
|
)
|
|
tab.Channel = channel;
|
|
}
|
|
}
|
|
|
|
var player = Plugin.ObjectTable.LocalPlayer;
|
|
if (tab.Channel == InputChannel.Tell && player != null)
|
|
{
|
|
ImGui.Checkbox(
|
|
Language.Options_Tabs_SenderMessages,
|
|
ref tab.AllSenderMessages
|
|
);
|
|
ImGuiUtil.HelpText(Language.Options_Help_SenderMessages);
|
|
|
|
var worlds = Sheets
|
|
.WorldsOnDatacenter(player)
|
|
.OrderByDescending(world => world.DataCenter.RowId)
|
|
.ThenBy(world => world.Name.ToString())
|
|
.ToList();
|
|
|
|
using (ImRaii.ItemWidth(ImGui.GetWindowWidth() / 3f))
|
|
{
|
|
ImGui.Text(Language.Options_Header_Target);
|
|
ImGui.SameLine();
|
|
|
|
var name = tab.TellTarget.Name;
|
|
if (ImGui.InputText("##targetInput", ref name, 21))
|
|
tab.TellTarget.Name = name;
|
|
|
|
ImGui.SameLine();
|
|
|
|
// Guard against an empty worlds list (character switch or sheet not yet populated)
|
|
// to avoid an out-of-bounds crash on worlds[selectedWorld].
|
|
if (worlds.Count == 0)
|
|
{
|
|
ImGui.TextDisabled("(no worlds available)");
|
|
}
|
|
else
|
|
{
|
|
var selectedWorld = worlds.FindIndex(world =>
|
|
world.RowId == tab.TellTarget.World
|
|
);
|
|
if (selectedWorld == -1)
|
|
selectedWorld = 0;
|
|
|
|
using (
|
|
var combo = ImRaii.Combo(
|
|
"###player-world",
|
|
worlds[selectedWorld].Name.ToString()
|
|
)
|
|
)
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
var lastDc = worlds.First().DataCenter.RowId;
|
|
foreach (var (idx, world) in worlds.Index())
|
|
{
|
|
if (
|
|
ImGui.Selectable(
|
|
world.Name.ToString(),
|
|
selectedWorld == idx
|
|
)
|
|
)
|
|
{
|
|
selectedWorld = idx;
|
|
tab.TellTarget.World = worlds[
|
|
selectedWorld
|
|
].RowId;
|
|
}
|
|
|
|
if (lastDc == world.DataCenter.RowId)
|
|
continue;
|
|
|
|
lastDc = world.DataCenter.RowId;
|
|
ImGui.Separator();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var target =
|
|
(Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target)
|
|
as IPlayerCharacter;
|
|
using (ImRaii.Disabled(target == null))
|
|
{
|
|
if (ImGui.Button("Set to target") && target != null)
|
|
tab.TellTarget.FromTarget(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// ── Sub-section: Pop-out window ───────────────────────────────────
|
|
if (sectionJustEntered)
|
|
ImGui.SetNextItemOpen(false);
|
|
using (
|
|
var secPopOut = ImRaii.TreeNode(
|
|
HellionStrings.Settings_Section_Tab_PopOut + $"##sec-popout-{i}"
|
|
)
|
|
)
|
|
{
|
|
if (secPopOut.Success)
|
|
{
|
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
|
|
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
|
|
if (tab.PopOut)
|
|
{
|
|
using var _ = ImRaii.PushIndent(10.0f);
|
|
ImGui.Checkbox(
|
|
Language.Options_Tabs_IndependentOpacity,
|
|
ref tab.IndependentOpacity
|
|
);
|
|
if (tab.IndependentOpacity)
|
|
ImGuiUtil.DragFloatVertical(
|
|
Language.Options_Tabs_Opacity,
|
|
ref tab.Opacity,
|
|
0.25f,
|
|
0f,
|
|
100f,
|
|
$"{tab.Opacity:N2}%%",
|
|
ImGuiSliderFlags.AlwaysClamp
|
|
);
|
|
|
|
ImGui.Checkbox(
|
|
Language.Options_Tabs_IndependentHide,
|
|
ref tab.IndependentHide
|
|
);
|
|
if (tab.IndependentHide)
|
|
{
|
|
using var __ = ImRaii.PushIndent(10.0f);
|
|
ImGuiUtil.OptionCheckbox(
|
|
ref tab.HideDuringCutscenes,
|
|
Language.Options_HideDuringCutscenes_Name
|
|
);
|
|
ImGui.Spacing();
|
|
|
|
ImGuiUtil.OptionCheckbox(
|
|
ref tab.HideWhenNotLoggedIn,
|
|
Language.Options_HideWhenNotLoggedIn_Name
|
|
);
|
|
ImGui.Spacing();
|
|
|
|
ImGuiUtil.OptionCheckbox(
|
|
ref tab.HideWhenUiHidden,
|
|
Language.Options_HideWhenUiHidden_Name
|
|
);
|
|
ImGui.Spacing();
|
|
|
|
ImGuiUtil.OptionCheckbox(
|
|
ref tab.HideInLoadingScreens,
|
|
Language.Options_HideInLoadingScreens_Name
|
|
);
|
|
ImGui.Spacing();
|
|
|
|
ImGuiUtil.OptionCheckbox(
|
|
ref tab.HideInBattle,
|
|
Language.Options_HideInBattle_Name
|
|
);
|
|
ImGui.Spacing();
|
|
}
|
|
|
|
ImGuiUtil.OptionCheckbox(ref tab.CanMove, Language.Popout_CanMove_Name);
|
|
ImGui.Spacing();
|
|
|
|
ImGuiUtil.OptionCheckbox(ref tab.CanResize, Language.Popout_CanResize_Name);
|
|
ImGui.Spacing();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (toRemove > -1)
|
|
{
|
|
Mutable.Tabs.RemoveAt(toRemove);
|
|
Plugin.WantedTab = 0;
|
|
}
|
|
|
|
if (doOpens)
|
|
ToOpen = -2;
|
|
}
|
|
}
|