Files
Shigure/UI/MainForm.cs

3413 lines
121 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Drawing;
namespace Shigure;
public sealed class MainForm : Form, IMessageFilter
{
private enum MainWindowLayout
{
Horizontal,
Vertical
}
private enum CloseButtonBehavior
{
MinimizeToTray,
Exit
}
private const int ResizeGripSize = 8;
private const int RoundedCornerResizeDebounceMs = 80;
private const int WowProcessMonitorIntervalMs = 10_000;
private const string HeaderIconResourcePath = "Assets.arasaka-icon-transparent.png";
private const string ModuleWebsiteUrl = "https://www.shigure.club";
private static readonly Color DefaultHeaderIconColor = Color.White;
private static readonly IReadOnlyDictionary<int, Color> ClassIconColors = new Dictionary<int, Color>
{
[1] = ColorTranslator.FromHtml("#C79C6E"),
[2] = ColorTranslator.FromHtml("#F58CBA"),
[3] = ColorTranslator.FromHtml("#ABD473"),
[4] = ColorTranslator.FromHtml("#FFF569"),
[5] = ColorTranslator.FromHtml("#FFFFFF"),
[6] = ColorTranslator.FromHtml("#C41F3B"),
[7] = ColorTranslator.FromHtml("#0070DE"),
[8] = ColorTranslator.FromHtml("#69CCF0"),
[9] = ColorTranslator.FromHtml("#9482C9"),
[10] = ColorTranslator.FromHtml("#00FF96"),
[11] = ColorTranslator.FromHtml("#FF7D0A"),
[12] = ColorTranslator.FromHtml("#A330C9"),
[13] = ColorTranslator.FromHtml("#33937F")
};
private Button _toggleKeyButton = null!;
private UiDropDown _modeComboBox = null!;
private UiDropDown _moduleComboBox = null!;
private Label _moduleFilterLabel = null!;
private Label _moduleCountLabel = null!;
private UiDropDown _defaultClassComboBox = null!;
private UiDropDown _defaultSpecComboBox = null!;
private UiDropDown _defaultHeroTalentComboBox = null!;
private UiDropDown _defaultPartyTypeComboBox = null!;
private UiDropDown _defaultModuleComboBox = null!;
private Button _setDefaultModuleButton = null!;
private Label _configSourceLabel = null!;
private Button _updateConfigButton = null!;
private Label _spellIconPackageStatusLabel = null!;
private Button _downloadSpellIconPackageButton = null!;
private readonly ToolTip _settingsToolTip = new();
private Button _horizontalLayoutButton = null!;
private Button _verticalLayoutButton = null!;
private Button _minimizeToTrayButton = null!;
private Button _exitOnCloseButton = null!;
private NotifyIcon _trayIcon = null!;
private ContextMenuStrip _trayMenu = null!;
private ToolStripMenuItem _trayToggleMenuItem = null!;
private Icon? _trayDefaultIcon;
private Icon? _trayEnabledIcon;
private bool? _trayIconShowsEnabled;
private string _toggleKeyName = "XBUTTON2";
private string? _selectedModuleId;
private bool _isCapturingToggleKey;
private bool _suppressModuleSelectionChanged;
private string? _lastModuleSelectorSignature;
private bool _usesDwmRoundedCorners = true;
private readonly List<Button> _enableButtons = [];
private Button _verticalEnableButton = null!;
private readonly List<PictureBox> _headerIcons = [];
private readonly List<Label> _titleLabels = [];
private readonly List<Label> _runtimeStatusLabels = [];
private Control _horizontalTopBar = null!;
private Control _verticalTopBar = null!;
private MainWindowLayout _mainWindowLayout = MainWindowLayout.Horizontal;
private CloseButtonBehavior _closeButtonBehavior = CloseButtonBehavior.MinimizeToTray;
private Bitmap? _headerIconMask;
private Color? _currentHeaderIconColor;
private readonly StatusForm _statusForm;
private readonly string _baseDirectory;
private readonly ModuleStore _moduleStore;
private readonly ITriggerKeyState _triggerKeyState;
private readonly WowProcessLocator _processLocator;
private readonly FuyutsuiAddonSyncService _addonSyncService;
private readonly ModuleDependencyService _moduleDependencyService;
private readonly RuntimeSessionCoordinator _runtimeSession;
private readonly ModuleEditorControl _moduleEditor;
private readonly ClassConfigEditorControl _classConfigEditor;
private readonly ClassMacrosEditorControl _classMacrosEditor;
private readonly AppOptions _initialOptions;
private readonly UiCacheState _uiCache;
private readonly System.Windows.Forms.Timer _roundedCornerResizeTimer;
private readonly System.Windows.Forms.Timer _wowProcessMonitorTimer;
private RenderSnapshot? _lastSnapshot;
private string? _lastLoggedStep;
private string? _lastLoggedStepDetails;
private string? _lastLoggedScanFailureReason;
private string? _lastLoggedClass;
private string? _lastLoggedModule;
private bool? _lastLoggedEnabled;
private readonly object _configUpdateSync = new();
private readonly SemaphoreSlim _moduleImportGate = new(1, 1);
private readonly SpellIconPackageDownloadService _spellIconPackageDownloadService = new();
private Task _configUpdateTail = Task.CompletedTask;
private Task _spellIconPackageDownloadTask = Task.CompletedTask;
private CancellationTokenSource? _spellIconPackageDownloadCts;
private long _runtimeRequestVersion;
private bool _exitRequested;
private bool _shutdownStarted;
private bool _shutdownCompleted;
private bool _wasWowProcessWindowAvailable;
private sealed record ProjectConfigUpdateResult(
FuyutsuiConfigConverter.UpdateResult Config,
FuyutsuiKeymapConverter.UpdateResult? Keymap,
FuyutsuiAddonSyncResult AddonSync);
private sealed record ClassModuleSaveResult(
int SavedCount,
IReadOnlyList<string> Warnings,
IReadOnlyList<string> Errors);
internal MainForm(
AppOptions initialOptions,
string baseDirectory,
ModuleStore moduleStore,
ITriggerKeyState triggerKeyState,
WowProcessLocator processLocator,
RuntimeSessionCoordinator runtimeSession)
{
_initialOptions = initialOptions;
_baseDirectory = baseDirectory;
_moduleStore = moduleStore;
_triggerKeyState = triggerKeyState;
_processLocator = processLocator;
var localAddonRoot = Path.Combine(_baseDirectory, "Fuyutsui");
_addonSyncService = new FuyutsuiAddonSyncService(localAddonRoot, _processLocator);
_moduleDependencyService = new ModuleDependencyService(_baseDirectory);
_runtimeSession = runtimeSession;
_uiCache = UiCacheStore.Load();
_statusForm = new StatusForm();
_roundedCornerResizeTimer = new System.Windows.Forms.Timer
{
Interval = RoundedCornerResizeDebounceMs
};
_roundedCornerResizeTimer.Tick += (_, _) =>
{
_roundedCornerResizeTimer.Stop();
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
UiTheme.ApplyFallbackRoundedCorners(this);
}
};
_wasWowProcessWindowAvailable = _processLocator.FindFrontmostWindow() != 0;
_wowProcessMonitorTimer = new System.Windows.Forms.Timer
{
Interval = WowProcessMonitorIntervalMs
};
_wowProcessMonitorTimer.Tick += HandleWowProcessMonitorTick;
_wowProcessMonitorTimer.Start();
Application.AddMessageFilter(this);
InitializeComponent();
TryApplyApplicationIcon();
InitializeTrayIcon();
_statusForm.AttachSettingsPanel(BuildSettingsPanel());
_moduleEditor = new ModuleEditorControl(
_moduleStore,
RestartRuntimeFromEditorAsync,
_moduleDependencyService.Capture,
ReloadModulesWithDependenciesAsync,
_baseDirectory);
_statusForm.AttachModuleEditor(_moduleEditor);
_classConfigEditor = new ClassConfigEditorControl(
() => Path.Combine(_addonSyncService.SourceRoot, "class"),
UpdateClassConfigAfterSaveAsync);
_statusForm.AttachConfigEditor(_classConfigEditor);
_classConfigEditor.DirtyStateChanged += dirty => _statusForm.SetPageDirty(SettingsPage.Config, dirty);
_classMacrosEditor = new ClassMacrosEditorControl(
() => Path.Combine(_addonSyncService.SourceRoot, "core", "classmacros.lua"),
UpdateClassConfigAfterSaveAsync);
_statusForm.AttachMacrosEditor(_classMacrosEditor);
_classMacrosEditor.DirtyStateChanged += dirty => _statusForm.SetPageDirty(SettingsPage.Macros, dirty);
_statusForm.FormClosing += (_, _) =>
{
CancelToggleKeyCapture();
SaveUiCache();
};
ApplyCachedWindowState();
ApplyInitialOptions();
WireSettingEvents();
_runtimeSession.SnapshotUpdated += HandleSnapshotUpdated;
_runtimeSession.RuntimeFailed += HandleRuntimeFailed;
_runtimeSession.RuntimeStopped += HandleRuntimeStopped;
SetRuntimeControls(running: false);
AppendLog("界面已就绪");
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
UiTheme.ApplyDarkTitleBar(this);
UiTheme.ApplyTranslucentBackground(this);
_usesDwmRoundedCorners = UiTheme.ApplyRoundedCorners(this);
}
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
var runtimeDataGenerated = await GenerateRuntimeDataAtStartupIfMissingAsync();
var dependenciesUpdated = await ImportModuleDependenciesAsync(reloadStore: true, showFeedback: true);
if (!dependenciesUpdated && !runtimeDataGenerated)
{
await SynchronizeAddonAtStartupAsync();
}
await StartRuntimeAsync();
}
private async Task<bool> GenerateRuntimeDataAtStartupIfMissingAsync()
{
var configDirectory = Path.Combine(_baseDirectory, ConfigService.ConfigDirectoryName);
var keymapDirectory = Path.Combine(_baseDirectory, "keymap");
var hasAllConfigFiles = Directory.Exists(configDirectory)
&& File.Exists(Path.Combine(configDirectory, ConfigService.CommonConfigFileName))
&& ClassNames.GetClasses().All(item =>
File.Exists(Path.Combine(configDirectory, $"{ClassNames.GetConfigFileName(item.Id)}.json")));
var hasAllKeymapFiles = Directory.Exists(keymapDirectory)
&& ClassNames.GetClasses().All(item =>
File.Exists(Path.Combine(
keymapDirectory,
$"{ClassNames.GetConfigFileName(item.Id).ToLowerInvariant()}.json")));
if (hasAllConfigFiles && hasAllKeymapFiles)
{
return false;
}
AppendLog("检测到 config 或 keymap 缺失或不完整,正在从项目 Fuyutsui 自动生成");
try
{
var result = await QueueProjectConfigUpdateAsync(savedAddonFilePath: null);
AppendLog(
$"已自动生成运行配置: config {result.Config.UpdatedFiles.Count} 个文件," +
$"keymap {result.Keymap?.UpdatedFiles.Count ?? 0} 个文件");
return true;
}
catch (Exception ex)
{
AppendLog($"自动生成 config/keymap 失败,程序将继续启动: {ex.Message}");
return false;
}
}
private Task ReloadModulesWithDependenciesAsync()
=> ImportModuleDependenciesAsync(reloadStore: true, showFeedback: true);
private async Task<bool> ImportModuleDependenciesAsync(bool reloadStore, bool showFeedback)
{
await _moduleImportGate.WaitAsync();
try
{
return await ImportModuleDependenciesCoreAsync(reloadStore, showFeedback);
}
finally
{
_moduleImportGate.Release();
}
}
private async Task<bool> ImportModuleDependenciesCoreAsync(bool reloadStore, bool showFeedback)
{
if (_classConfigEditor.HasUnsavedChanges || _classMacrosEditor.HasUnsavedChanges)
{
if (showFeedback)
{
MessageBox.Show(
"配置或宏页面存在未保存修改。请先保存或放弃修改,再刷新模块。",
"模块依赖未导入",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
return false;
}
if (reloadStore)
{
_moduleStore.Reload();
}
ModuleDependencyImportResult result;
try
{
// 合并阶段保持在 UI 线程,避免配置/宏编辑器在检查脏状态后又并发写同一 Lua。
result = _moduleDependencyService.Import(_moduleStore.GetModules());
}
catch (Exception ex)
{
AppendLog($"模块依赖导入失败: {ex.Message}");
if (showFeedback)
{
MessageBox.Show(ex.Message, "模块依赖导入失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return false;
}
var cleanedModuleCount = 0;
var cleanupErrors = new List<string>();
foreach (var module in result.SanitizedModules)
{
try
{
_moduleStore.SaveDependenciesInPlace(module);
cleanedModuleCount++;
}
catch (Exception ex)
{
cleanupErrors.Add($"{module.Name}: {ex.Message}");
}
}
_moduleStore.SetImportIssues(
result.Rejected.Select(item => item.ModuleId),
result.ConflictedModuleIds);
_moduleEditor.ReloadModulesFromStore(reloadStore: false);
RefreshModuleSelector(_lastSnapshot, forceRefresh: false);
foreach (var rejected in result.Rejected)
{
AppendLog($"模块“{rejected.ModuleName}”未导入: {rejected.Reason}");
}
foreach (var conflict in result.Conflicts.Take(50))
{
AppendLog($"模块依赖冲突: {conflict}");
}
foreach (var field in result.RemovedStateFields.Take(50))
{
AppendLog($"模块“{field.ModuleName}”已忽略未识别配置字段: {field.Category}.{field.Name}");
}
if (result.RemovedStateFields.Count > 0)
{
AppendLog($"已从 {cleanedModuleCount} 个模块删除 {result.RemovedStateFields.Count} 个未识别配置字段,未导入本地配置。");
}
foreach (var error in cleanupErrors)
{
AppendLog($"模块依赖字段清理回写失败: {error}");
}
string? postUpdateError = null;
if (result.HasChanges)
{
AppendLog(
$"已从模块补充本地依赖: 配置新增 {result.ConfigAdded} 项、整理 {result.ConfigUpdated} 项,宏 {result.MacrosAdded} 项;模块 {string.Join("", result.ChangedModules)}");
_classConfigEditor.ReloadFromAddon();
_classMacrosEditor.ReloadFromAddon();
try
{
await QueueProjectConfigUpdateAsync(savedAddonFilePath: null);
}
catch (Exception ex)
{
postUpdateError = ex.Message;
AppendLog($"模块依赖已写入,但后续配置更新失败: {ex.Message}");
}
}
if (showFeedback && (result.HasChanges
|| result.Rejected.Count > 0
|| result.Conflicts.Count > 0
|| result.RemovedStateFields.Count > 0))
{
var lines = new List<string>();
if (result.HasChanges)
{
lines.Add($"成功处理配置:新增 {result.ConfigAdded} 项、整理 {result.ConfigUpdated} 项;宏 {result.MacrosAdded} 项。");
}
if (result.Rejected.Count > 0)
{
lines.Add("未导入模块:");
lines.AddRange(result.Rejected.Select(item => $"- {item.ModuleName}: {item.Reason}"));
}
if (result.Conflicts.Count > 0)
{
lines.Add($"发现 {result.Conflicts.Count} 项冲突,均已保留本地内容;详情见日志。");
}
if (result.RemovedStateFields.Count > 0)
{
lines.Add($"已忽略 {result.RemovedStateFields.Count} 个未识别配置字段,并从 {cleanedModuleCount} 个模块中删除;这些字段未写入本地配置。");
}
if (cleanupErrors.Count > 0)
{
lines.Add($"有 {cleanupErrors.Count} 个模块无法回写清理结果;本次仍未导入这些未识别字段,详情见日志。");
}
if (!string.IsNullOrWhiteSpace(postUpdateError))
{
lines.Add($"本地依赖已写入,但 config/keymap 或游戏同步更新失败:{postUpdateError}");
}
var hasWarning = result.Rejected.Count > 0 || cleanupErrors.Count > 0 || postUpdateError is not null;
MessageBox.Show(
string.Join(Environment.NewLine, lines),
hasWarning ? "模块导入完成(有警告)" : "模块导入完成",
MessageBoxButtons.OK,
hasWarning ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
}
return result.HasChanges;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!_shutdownCompleted
&& !_exitRequested
&& e.CloseReason == CloseReason.UserClosing
&& _closeButtonBehavior == CloseButtonBehavior.MinimizeToTray)
{
e.Cancel = true;
MinimizeToTray();
base.OnFormClosing(e);
return;
}
if (!_shutdownCompleted)
{
e.Cancel = true;
if (!_shutdownStarted)
{
_shutdownStarted = true;
SaveUiCache();
_roundedCornerResizeTimer.Stop();
_wowProcessMonitorTimer.Stop();
_spellIconPackageDownloadCts?.Cancel();
Application.RemoveMessageFilter(this);
_ = CompleteShutdownAsync();
}
base.OnFormClosing(e);
return;
}
base.OnFormClosing(e);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_trayIcon.Visible = false;
_trayIcon.Dispose();
_trayMenu.Dispose();
_trayDefaultIcon?.Dispose();
_trayEnabledIcon?.Dispose();
_roundedCornerResizeTimer.Dispose();
_wowProcessMonitorTimer.Dispose();
base.OnFormClosed(e);
}
private async void HandleWowProcessMonitorTick(object? sender, EventArgs e)
{
var isAvailable = _processLocator.FindFrontmostWindow() != 0;
var justOpened = !_wasWowProcessWindowAvailable && isAvailable;
_wasWowProcessWindowAvailable = isAvailable;
if (!justOpened || _shutdownStarted)
{
return;
}
AppendLog("检测到目标游戏进程已打开,正在自动更新配置");
try
{
await QueueProjectConfigUpdateAsync(savedAddonFilePath: null);
if (!_shutdownStarted)
{
AppendLog("目标游戏进程启动后的配置更新已完成");
}
}
catch (OperationCanceledException) when (_shutdownStarted)
{
// 关闭流程会等待队列收尾,无需再记录失败。
}
catch (Exception ex)
{
if (!_shutdownStarted)
{
AppendLog($"目标游戏进程启动后的配置更新失败: {ex.Message}");
}
}
}
private async Task CompleteShutdownAsync()
{
_runtimeSession.SnapshotUpdated -= HandleSnapshotUpdated;
_runtimeSession.RuntimeFailed -= HandleRuntimeFailed;
_runtimeSession.RuntimeStopped -= HandleRuntimeStopped;
try
{
var runtimeShutdown = _runtimeSession.DisposeAsync().AsTask();
await Task.WhenAll(
runtimeShutdown,
GetPendingConfigUpdateTask(),
_spellIconPackageDownloadTask);
}
catch (Exception ex)
{
AppendLog($"停止运行失败: {ex.Message}");
}
finally
{
_statusForm.Dispose();
_spellIconPackageDownloadCts?.Dispose();
_spellIconPackageDownloadService.Dispose();
_shutdownCompleted = true;
if (!IsDisposed)
{
Close();
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
ScheduleFallbackRoundedCornerUpdate();
}
}
protected override void OnResizeEnd(EventArgs e)
{
base.OnResizeEnd(e);
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
_roundedCornerResizeTimer.Stop();
UiTheme.ApplyFallbackRoundedCorners(this);
}
}
private void ScheduleFallbackRoundedCornerUpdate()
{
_roundedCornerResizeTimer.Stop();
_roundedCornerResizeTimer.Start();
}
protected override void WndProc(ref Message m)
{
const int WmNcHitTest = 0x0084;
if (m.Msg == WmNcHitTest)
{
base.WndProc(ref m);
if (m.Result == NativeMethods.HtClient)
{
m.Result = HitTestResizeGrip(PointToClient(Cursor.Position));
}
return;
}
base.WndProc(ref m);
}
private void InitializeComponent()
{
SuspendLayout();
Text = "Shigure";
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
ClientSize = new Size(680, 64);
MinimumSize = new Size(420, 56);
BackColor = Color.FromArgb(18, 21, 26);
ForeColor = UiTheme.Text;
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
var root = new Panel
{
Dock = DockStyle.Fill,
BackColor = Color.Transparent,
Padding = new Padding(12),
Margin = new Padding(0)
};
Controls.Add(root);
root.Controls.Add(BuildTopBars());
ResumeLayout(false);
}
private Control BuildTopBars()
{
var host = new Panel
{
Dock = DockStyle.Fill,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
_horizontalTopBar = BuildHorizontalTopBar();
_verticalTopBar = BuildVerticalTopBar();
_verticalTopBar.Visible = false;
host.Controls.Add(_horizontalTopBar);
host.Controls.Add(_verticalTopBar);
_currentHeaderIconColor = null;
UpdateHeaderIconColor(null);
return host;
}
private Control BuildHorizontalTopBar()
{
var bar = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 1,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
bar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
bar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
bar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
bar.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
var brand = new FlowLayoutPanel
{
AutoSize = true,
Anchor = AnchorStyles.Left,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0, 0, 12, 0),
Padding = new Padding(0)
};
var headerIcon = CreateHeaderIcon();
var titleLabel = new Label
{
Text = "Shigure",
AutoSize = true,
Anchor = AnchorStyles.Left,
Font = new Font(Font.FontFamily, 13F, FontStyle.Bold),
ForeColor = UiTheme.Text,
Margin = new Padding(8, 0, 0, 0)
};
var runtimeStatusLabel = new Label
{
Text = string.Empty,
AutoSize = false,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = UiTheme.Muted
};
brand.Controls.Add(headerIcon);
brand.Controls.Add(titleLabel);
var buttons = BuildTopBarButtons(vertical: false);
RegisterTopBarPresentation(headerIcon, titleLabel, runtimeStatusLabel);
EnableDrag(bar);
EnableDrag(brand);
EnableDrag(headerIcon);
EnableDrag(titleLabel);
EnableDrag(runtimeStatusLabel);
bar.Controls.Add(brand, 0, 0);
bar.Controls.Add(runtimeStatusLabel, 1, 0);
bar.Controls.Add(buttons, 2, 0);
return bar;
}
private Control BuildVerticalTopBar()
{
var bar = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
bar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
bar.RowStyles.Add(new RowStyle(SizeType.AutoSize));
bar.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
bar.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var brand = new FlowLayoutPanel
{
AutoSize = true,
Anchor = AnchorStyles.Top,
FlowDirection = FlowDirection.TopDown,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
var headerIcon = CreateHeaderIcon();
headerIcon.Anchor = AnchorStyles.Top;
var titleFont = new Font(Font.FontFamily, 13F, FontStyle.Bold);
// 旋转后由 GDI+ 绘制文字,保留 GDI 的默认字形外延并额外留出少量边距,
// 避免末尾字符因两套文字测量方式的差异被裁掉。
var titleSize = TextRenderer.MeasureText("Shigure", titleFont);
var titleLabel = new RotatableLabel
{
Text = "Shigure",
AutoSize = false,
Size = new Size(titleSize.Width + 4, 32),
Anchor = AnchorStyles.Top,
Font = titleFont,
ForeColor = UiTheme.Text,
Margin = new Padding(0, 8, 0, 0)
};
titleLabel.Rotated = true;
var runtimeStatusLabel = new RotatableLabel
{
Text = string.Empty,
AutoSize = false,
Dock = DockStyle.Fill,
ForeColor = UiTheme.Muted
};
runtimeStatusLabel.Rotated = true;
brand.Controls.Add(headerIcon);
brand.Controls.Add(titleLabel);
var buttons = BuildTopBarButtons(vertical: true);
RegisterTopBarPresentation(headerIcon, titleLabel, runtimeStatusLabel);
EnableDrag(bar);
EnableDrag(brand);
EnableDrag(headerIcon);
EnableDrag(titleLabel);
EnableDrag(runtimeStatusLabel);
bar.Controls.Add(brand, 0, 0);
bar.Controls.Add(runtimeStatusLabel, 0, 1);
bar.Controls.Add(buttons, 0, 2);
return bar;
}
private FlowLayoutPanel BuildTopBarButtons(bool vertical)
{
var buttons = new FlowLayoutPanel
{
AutoSize = true,
Anchor = vertical ? AnchorStyles.Bottom : AnchorStyles.Right,
FlowDirection = vertical ? FlowDirection.TopDown : FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
var enableButton = CreateTopBarButton(vertical ? "开\r\n启" : "开关", UiTheme.Field, UiTheme.Text, vertical);
enableButton.Click += (_, _) => ToggleEnabled();
var settingsButton = CreateTopBarButton(vertical ? "设\r\n置" : "设置", UiTheme.Field, UiTheme.Text, vertical);
settingsButton.Click += (_, _) => ShowSettingsView();
var closeButton = CreateTopBarButton(vertical ? "X" : "✕", UiTheme.Field, UiTheme.Muted, vertical);
closeButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(196, 43, 28);
closeButton.FlatAppearance.MouseDownBackColor = Color.FromArgb(153, 27, 21);
closeButton.Click += (_, _) => Close();
_enableButtons.Add(enableButton);
if (vertical)
{
_verticalEnableButton = enableButton;
}
buttons.Controls.AddRange([enableButton, settingsButton, closeButton]);
return buttons;
}
private void RegisterTopBarPresentation(PictureBox icon, Label title, Label status)
{
_headerIcons.Add(icon);
_titleLabels.Add(title);
_runtimeStatusLabels.Add(status);
}
private static PictureBox CreateHeaderIcon()
{
var box = new PictureBox
{
Size = new Size(32, 32),
MinimumSize = new Size(32, 32),
MaximumSize = new Size(32, 32),
SizeMode = PictureBoxSizeMode.Zoom,
BackColor = Color.Transparent,
Margin = new Padding(0),
Anchor = AnchorStyles.Left
};
return box;
}
private void UpdateHeaderIconColor(int? classId)
{
var color = ResolveClassIconColor(classId);
if (_currentHeaderIconColor == color)
{
return;
}
_currentHeaderIconColor = color;
_headerIconMask ??= LoadHeaderIconMask();
if (_headerIconMask is null)
{
return;
}
foreach (var headerIcon in _headerIcons)
{
var previous = headerIcon.Image;
headerIcon.Image = TintHeaderIcon(_headerIconMask, color);
previous?.Dispose();
}
}
private static Color ResolveClassIconColor(int? classId)
=> classId is not null && ClassIconColors.TryGetValue(classId.Value, out var color)
? color
: DefaultHeaderIconColor;
private static Bitmap? LoadHeaderIconMask()
{
using var stream = typeof(MainForm).Assembly.GetManifestResourceStream(GetHeaderIconResourceName());
if (stream is null)
{
return null;
}
using var image = Image.FromStream(stream);
return new Bitmap(image);
}
private static string GetHeaderIconResourceName() =>
$"{typeof(MainForm).Namespace}.{HeaderIconResourcePath}";
private static Bitmap TintHeaderIcon(Bitmap mask, Color color)
{
var bitmap = new Bitmap(mask.Width, mask.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
bitmap.SetResolution(mask.HorizontalResolution, mask.VerticalResolution);
for (var y = 0; y < mask.Height; y++)
{
for (var x = 0; x < mask.Width; x++)
{
var pixel = mask.GetPixel(x, y);
if (pixel.A == 0)
{
continue;
}
bitmap.SetPixel(x, y, Color.FromArgb(pixel.A, color));
}
}
return bitmap;
}
private Control BuildSettingsPanel()
{
var scrollHost = new Panel
{
Dock = DockStyle.Fill,
BackColor = UiTheme.Surface,
AutoScroll = true,
Margin = new Padding(0)
};
var panel = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = UiTheme.Surface,
ColumnCount = 2,
RowCount = 4,
Padding = new Padding(0),
Margin = new Padding(0)
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
const int settingsCardHeight = 190;
const int settingsCardGap = UiTheme.PageGap;
const int settingsActionButtonHeight = UiTheme.ActionButtonHeight;
for (var row = 0; row < panel.RowCount; row++)
{
var rowHeight = settingsCardHeight + (row == panel.RowCount - 1 ? 0 : settingsCardGap);
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, rowHeight));
}
UiCardPanel CreateSettingsCard(int column, int row, int columnCount = 1)
{
return new UiCardPanel
{
Dock = DockStyle.Fill,
ColumnCount = columnCount,
RowCount = 4,
Padding = new Padding(UiTheme.CardPadding),
Margin = new Padding(
column == 0 ? 0 : settingsCardGap / 2,
0,
column == 0 ? settingsCardGap / 2 : 0,
row == panel.RowCount - 1 ? 0 : settingsCardGap)
};
}
static void ConfigureSettingsCardRows(UiCardPanel card, params int?[] heights)
{
card.RowStyles.Clear();
foreach (var height in heights)
{
card.RowStyles.Add(height is null
? new RowStyle(SizeType.Percent, 100)
: new RowStyle(SizeType.Absolute, height.Value));
}
}
Label CreateTitle(string text) => UiTheme.CreateSectionTitle(Font, text);
Label CreateDescription(string text) => UiTheme.CreateDescription(text);
Label CreateSettingLabel(string text) => new()
{
Text = text,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = UiTheme.Muted,
Margin = new Padding(0)
};
var inputCard = CreateSettingsCard(column: 0, row: 0, columnCount: 2);
inputCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 100));
inputCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
ConfigureSettingsCardRows(inputCard, 32, 30, settingsActionButtonHeight + 8, settingsActionButtonHeight + 8);
inputCard.Controls.Add(CreateTitle("输入与运行"), 0, 0);
inputCard.SetColumnSpan(inputCard.GetControlFromPosition(0, 0)!, 2);
inputCard.Controls.Add(CreateDescription("设置触发方式;修改后运行循环会自动重启"), 0, 1);
inputCard.SetColumnSpan(inputCard.GetControlFromPosition(0, 1)!, 2);
_toggleKeyButton = UiTheme.CreateButton("XBUTTON2", UiTheme.ButtonKind.Secondary);
_toggleKeyButton.AutoSize = false;
_toggleKeyButton.Size = new Size(190, settingsActionButtonHeight);
_toggleKeyButton.TextAlign = ContentAlignment.MiddleCenter;
_toggleKeyButton.Anchor = AnchorStyles.Left;
_toggleKeyButton.Margin = new Padding(0);
_toggleKeyButton.Click += (_, _) => BeginCaptureToggleKey();
_settingsToolTip.SetToolTip(_toggleKeyButton, "点击后按下新的键盘键或鼠标侧键");
inputCard.Controls.Add(CreateSettingLabel("触发键"), 0, 2);
inputCard.Controls.Add(_toggleKeyButton, 1, 2);
_modeComboBox = new UiDropDown();
UiTheme.StyleComboBox(_modeComboBox);
_modeComboBox.Items.AddRange(new object[] { "开关", "单击", "按住" });
_modeComboBox.SelectedIndex = 0;
_modeComboBox.Width = 190;
_modeComboBox.Anchor = AnchorStyles.Left;
_modeComboBox.Margin = new Padding(0);
_settingsToolTip.SetToolTip(_modeComboBox, "开关:按一次切换;单击:每次触发发送一次;按住:持续按下时运行");
inputCard.Controls.Add(CreateSettingLabel("发送模式"), 0, 3);
inputCard.Controls.Add(_modeComboBox, 1, 3);
var configCard = CreateSettingsCard(column: 1, row: 0);
ConfigureSettingsCardRows(configCard, 32, 30, null, settingsActionButtonHeight);
configCard.Controls.Add(CreateTitle("配置同步"), 0, 0);
configCard.Controls.Add(CreateDescription("从项目 Fuyutsui 生成 config/keymap并同步到游戏"), 0, 1);
_configSourceLabel = CreateInfoLabel("项目目录是唯一配置源;尚未执行手动更新");
_configSourceLabel.Dock = DockStyle.Fill;
_configSourceLabel.AutoSize = false;
_configSourceLabel.AutoEllipsis = true;
_configSourceLabel.TextAlign = ContentAlignment.TopLeft;
_configSourceLabel.Margin = new Padding(0, 10, 0, 8);
_settingsToolTip.SetToolTip(_configSourceLabel, _configSourceLabel.Text);
configCard.Controls.Add(_configSourceLabel, 0, 2);
_updateConfigButton = UiTheme.CreateButton("更新配置", UiTheme.ButtonKind.Secondary);
_updateConfigButton.AutoSize = false;
_updateConfigButton.Size = new Size(122, settingsActionButtonHeight);
_updateConfigButton.Dock = DockStyle.Left;
_updateConfigButton.Margin = new Padding(0);
_updateConfigButton.Click += async (_, _) => await UpdateConfigFromProjectWithFeedbackAsync();
configCard.Controls.Add(_updateConfigButton, 0, 3);
var moduleCard = CreateSettingsCard(column: 0, row: 1, columnCount: 2);
moduleCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
moduleCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 132));
ConfigureSettingsCardRows(moduleCard, 32, 30, settingsActionButtonHeight, null);
moduleCard.Controls.Add(CreateTitle("模块选择"), 0, 0);
moduleCard.SetColumnSpan(moduleCard.GetControlFromPosition(0, 0)!, 2);
moduleCard.Controls.Add(CreateDescription("按实时职业与专精自动匹配,或手动指定模块"), 0, 1);
moduleCard.SetColumnSpan(moduleCard.GetControlFromPosition(0, 1)!, 2);
_moduleComboBox = new UiDropDown();
UiTheme.StyleComboBox(_moduleComboBox);
_moduleComboBox.Dock = DockStyle.Fill;
_moduleComboBox.Margin = new Padding(0, 0, 14, 0);
_settingsToolTip.SetToolTip(_moduleComboBox, "列表会根据当前游戏状态筛选可用模块");
moduleCard.Controls.Add(_moduleComboBox, 0, 2);
var refreshModulesButton = UiTheme.CreateButton("刷新模块", UiTheme.ButtonKind.Secondary);
refreshModulesButton.AutoSize = false;
refreshModulesButton.Dock = DockStyle.Fill;
refreshModulesButton.Margin = new Padding(0);
refreshModulesButton.Click += async (_, _) =>
{
await ReloadModulesWithDependenciesAsync();
RefreshModuleSelector(_lastSnapshot, forceRefresh: false);
RefreshDefaultModuleSelector();
};
moduleCard.Controls.Add(refreshModulesButton, 1, 2);
var moduleInfoText = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
FlowDirection = FlowDirection.TopDown,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0, 8, 0, 0)
};
_moduleFilterLabel = CreateInfoLabel("筛选: 等待游戏状态");
_moduleCountLabel = CreateInfoLabel("可选模块: 0");
moduleInfoText.Controls.Add(_moduleFilterLabel);
moduleInfoText.Controls.Add(_moduleCountLabel);
moduleCard.Controls.Add(moduleInfoText, 0, 3);
moduleCard.SetColumnSpan(moduleInfoText, 2);
var getModulesCard = CreateSettingsCard(column: 1, row: 2);
ConfigureSettingsCardRows(getModulesCard, 32, 30, null, settingsActionButtonHeight);
getModulesCard.Controls.Add(CreateTitle("获取模块"), 0, 0);
getModulesCard.Controls.Add(CreateDescription("访问 Shigure 官网,浏览并获取可用模块"), 0, 1);
var moduleWebsiteLabel = CreateInfoLabel(ModuleWebsiteUrl);
moduleWebsiteLabel.Dock = DockStyle.Fill;
moduleWebsiteLabel.AutoSize = false;
moduleWebsiteLabel.AutoEllipsis = true;
moduleWebsiteLabel.TextAlign = ContentAlignment.TopLeft;
moduleWebsiteLabel.ForeColor = UiTheme.Accent;
moduleWebsiteLabel.Cursor = Cursors.Hand;
moduleWebsiteLabel.Margin = new Padding(0, 10, 0, 8);
moduleWebsiteLabel.Click += (_, _) => OpenModuleWebsite();
_settingsToolTip.SetToolTip(moduleWebsiteLabel, $"在默认浏览器中打开 {ModuleWebsiteUrl}");
getModulesCard.Controls.Add(moduleWebsiteLabel, 0, 2);
var moduleActions = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = false,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
var moduleWebsiteButtonColor = Color.FromArgb(252, 238, 10);
var openModuleWebsiteButton = UiTheme.CreateButton("获取模块", moduleWebsiteButtonColor, Color.Black);
openModuleWebsiteButton.AutoSize = false;
openModuleWebsiteButton.Size = new Size(160, settingsActionButtonHeight);
openModuleWebsiteButton.Margin = new Padding(0, 0, 10, 0);
openModuleWebsiteButton.Padding = new Padding(0, 2, 24, 2);
openModuleWebsiteButton.FlatAppearance.BorderColor = moduleWebsiteButtonColor;
openModuleWebsiteButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(255, 244, 64);
openModuleWebsiteButton.FlatAppearance.MouseDownBackColor = Color.FromArgb(220, 207, 8);
openModuleWebsiteButton.Paint += (_, e) => UiTheme.DrawExternalLinkIcon(
e.Graphics,
openModuleWebsiteButton.ClientRectangle,
openModuleWebsiteButton.Text,
openModuleWebsiteButton.Font,
openModuleWebsiteButton.ForeColor,
openModuleWebsiteButton.DeviceDpi / 96F);
openModuleWebsiteButton.Click += (_, _) => OpenModuleWebsite();
var openModuleDirectoryButton = UiTheme.CreateButton("打开模块目录", UiTheme.ButtonKind.Secondary);
openModuleDirectoryButton.AutoSize = false;
openModuleDirectoryButton.Size = new Size(160, settingsActionButtonHeight);
openModuleDirectoryButton.Margin = new Padding(0);
openModuleDirectoryButton.Click += (_, _) => OpenModuleDirectory();
_settingsToolTip.SetToolTip(openModuleDirectoryButton, "在资源管理器中打开本地模块目录");
moduleActions.Controls.Add(openModuleWebsiteButton);
moduleActions.Controls.Add(openModuleDirectoryButton);
getModulesCard.Controls.Add(moduleActions, 0, 3);
var defaultModuleCard = CreateSettingsCard(column: 1, row: 1);
ConfigureSettingsCardRows(defaultModuleCard, 32, 30, 48, settingsActionButtonHeight);
defaultModuleCard.Controls.Add(CreateTitle("默认模块"), 0, 0);
defaultModuleCard.Controls.Add(
CreateDescription("为指定环境设置自动选择时优先使用的模块"),
0,
1);
var defaultFilterRow = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 4,
RowCount = 1,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
for (var i = 0; i < 4; i++)
{
defaultFilterRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25));
}
_defaultClassComboBox = CreateDefaultFilterComboBox();
_defaultSpecComboBox = CreateDefaultFilterComboBox();
_defaultHeroTalentComboBox = CreateDefaultFilterComboBox();
_defaultPartyTypeComboBox = CreateDefaultFilterComboBox();
defaultFilterRow.Controls.Add(_defaultClassComboBox, 0, 0);
defaultFilterRow.Controls.Add(_defaultSpecComboBox, 1, 0);
defaultFilterRow.Controls.Add(_defaultHeroTalentComboBox, 2, 0);
defaultFilterRow.Controls.Add(_defaultPartyTypeComboBox, 3, 0);
_settingsToolTip.SetToolTip(_defaultClassComboBox, "职业");
_settingsToolTip.SetToolTip(_defaultSpecComboBox, "专精");
_settingsToolTip.SetToolTip(_defaultHeroTalentComboBox, "英雄天赋");
_settingsToolTip.SetToolTip(_defaultPartyTypeComboBox, "队伍类型");
defaultModuleCard.Controls.Add(defaultFilterRow, 0, 2);
var defaultModuleRow = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 1,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
defaultModuleRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
defaultModuleRow.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 132));
_defaultModuleComboBox = new UiDropDown();
UiTheme.StyleComboBox(_defaultModuleComboBox);
_defaultModuleComboBox.Dock = DockStyle.Fill;
_defaultModuleComboBox.Margin = new Padding(0, 0, 14, 0);
_settingsToolTip.SetToolTip(_defaultModuleComboBox, "列表按上方条件筛选;已保存的模块会标记为“当前默认”");
defaultModuleRow.Controls.Add(_defaultModuleComboBox, 0, 0);
_setDefaultModuleButton = UiTheme.CreateButton("设为默认", UiTheme.ButtonKind.Secondary);
_setDefaultModuleButton.AutoSize = false;
_setDefaultModuleButton.Dock = DockStyle.Fill;
_setDefaultModuleButton.Margin = new Padding(0);
_setDefaultModuleButton.Click += HandleSetDefaultModuleClick;
defaultModuleRow.Controls.Add(_setDefaultModuleButton, 1, 0);
defaultModuleCard.Controls.Add(defaultModuleRow, 0, 3);
ResetDefaultClassOptions();
ResetDefaultSpecOptions(null);
ResetDefaultHeroTalentOptions(null, null);
ResetDefaultPartyTypeOptions();
_defaultClassComboBox.SelectedIndexChanged += (_, _) =>
{
var classId = ReadDefaultFilterValue(_defaultClassComboBox);
ResetDefaultSpecOptions(classId);
ResetDefaultHeroTalentOptions(classId, ReadDefaultFilterValue(_defaultSpecComboBox));
RefreshDefaultModuleSelector();
};
_defaultSpecComboBox.SelectedIndexChanged += (_, _) =>
{
ResetDefaultHeroTalentOptions(
ReadDefaultFilterValue(_defaultClassComboBox),
ReadDefaultFilterValue(_defaultSpecComboBox));
RefreshDefaultModuleSelector();
};
_defaultHeroTalentComboBox.SelectedIndexChanged += (_, _) => RefreshDefaultModuleSelector();
_defaultPartyTypeComboBox.SelectedIndexChanged += (_, _) => RefreshDefaultModuleSelector();
var layoutCard = CreateSettingsCard(column: 0, row: 2);
ConfigureSettingsCardRows(layoutCard, 32, 30, null, settingsActionButtonHeight);
layoutCard.Controls.Add(CreateTitle("界面布局"), 0, 0);
layoutCard.Controls.Add(CreateDescription("选择主界面浮动条的排列方向"), 0, 1);
var layoutInfoLabel = CreateInfoLabel("切换时会交换主界面的宽高,控件与功能保持不变");
layoutInfoLabel.Dock = DockStyle.Fill;
layoutInfoLabel.AutoSize = false;
layoutInfoLabel.TextAlign = ContentAlignment.TopLeft;
layoutInfoLabel.Margin = new Padding(0, 10, 0, 8);
layoutCard.Controls.Add(layoutInfoLabel, 0, 2);
var layoutActions = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
_horizontalLayoutButton = UiTheme.CreateButton("横向布局", UiTheme.ButtonKind.Secondary);
_horizontalLayoutButton.AutoSize = false;
_horizontalLayoutButton.Size = new Size(140, settingsActionButtonHeight);
_horizontalLayoutButton.Margin = new Padding(0, 0, 10, 0);
_horizontalLayoutButton.Click += (_, _) => SetMainWindowLayout(MainWindowLayout.Horizontal);
_verticalLayoutButton = UiTheme.CreateButton("纵向布局", UiTheme.ButtonKind.Secondary);
_verticalLayoutButton.AutoSize = false;
_verticalLayoutButton.Size = new Size(140, settingsActionButtonHeight);
_verticalLayoutButton.Margin = new Padding(0);
_verticalLayoutButton.Click += (_, _) => SetMainWindowLayout(MainWindowLayout.Vertical);
layoutActions.Controls.Add(_horizontalLayoutButton);
layoutActions.Controls.Add(_verticalLayoutButton);
layoutCard.Controls.Add(layoutActions, 0, 3);
panel.Controls.Add(inputCard, 0, 0);
panel.Controls.Add(configCard, 1, 0);
panel.Controls.Add(moduleCard, 0, 1);
panel.Controls.Add(defaultModuleCard, 1, 1);
panel.Controls.Add(layoutCard, 0, 2);
panel.Controls.Add(getModulesCard, 1, 2);
var closeBehaviorCard = CreateSettingsCard(column: 1, row: 3);
ConfigureSettingsCardRows(closeBehaviorCard, 32, 30, null, settingsActionButtonHeight);
closeBehaviorCard.Controls.Add(CreateTitle("点击 X 时(关闭窗口按钮)"), 0, 0);
closeBehaviorCard.Controls.Add(CreateDescription("选择点击主界面关闭按钮后的行为"), 0, 1);
var closeBehaviorInfo = CreateInfoLabel("最小化后可通过系统栏图标重新打开;完全退出会停止运行");
closeBehaviorInfo.Dock = DockStyle.Fill;
closeBehaviorInfo.AutoSize = false;
closeBehaviorInfo.TextAlign = ContentAlignment.TopLeft;
closeBehaviorInfo.Margin = new Padding(0, 10, 0, 8);
closeBehaviorCard.Controls.Add(closeBehaviorInfo, 0, 2);
var closeBehaviorActions = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0),
Padding = new Padding(0)
};
_minimizeToTrayButton = UiTheme.CreateButton("最小化到系统栏", UiTheme.ButtonKind.Secondary);
_minimizeToTrayButton.AutoSize = false;
_minimizeToTrayButton.Size = new Size(160, settingsActionButtonHeight);
_minimizeToTrayButton.Margin = new Padding(0, 0, 10, 0);
_minimizeToTrayButton.Click += (_, _) => SetCloseButtonBehavior(CloseButtonBehavior.MinimizeToTray);
_exitOnCloseButton = UiTheme.CreateButton("完全退出Shigure", UiTheme.ButtonKind.Secondary);
_exitOnCloseButton.AutoSize = false;
_exitOnCloseButton.Size = new Size(160, settingsActionButtonHeight);
_exitOnCloseButton.Margin = new Padding(0);
_exitOnCloseButton.Click += (_, _) => SetCloseButtonBehavior(CloseButtonBehavior.Exit);
closeBehaviorActions.Controls.Add(_minimizeToTrayButton);
closeBehaviorActions.Controls.Add(_exitOnCloseButton);
closeBehaviorCard.Controls.Add(closeBehaviorActions, 0, 3);
panel.Controls.Add(closeBehaviorCard, 1, 3);
var spellIconPackageCard = CreateSettingsCard(column: 0, row: 3);
ConfigureSettingsCardRows(spellIconPackageCard, 32, 30, null, settingsActionButtonHeight);
spellIconPackageCard.Controls.Add(CreateTitle("下载数据包"), 0, 0);
spellIconPackageCard.Controls.Add(
CreateDescription("从 GitHub Release 下载或更新技能/物品图标数据包API 不可用时回退到发布页直链"),
0,
1);
_spellIconPackageStatusLabel = CreateInfoLabel(string.Empty);
_spellIconPackageStatusLabel.Dock = DockStyle.Fill;
_spellIconPackageStatusLabel.AutoSize = false;
_spellIconPackageStatusLabel.AutoEllipsis = true;
_spellIconPackageStatusLabel.TextAlign = ContentAlignment.TopLeft;
_spellIconPackageStatusLabel.Margin = new Padding(0, 10, 0, 8);
spellIconPackageCard.Controls.Add(_spellIconPackageStatusLabel, 0, 2);
_downloadSpellIconPackageButton = UiTheme.CreateButton(
"下载数据包",
UiTheme.ButtonKind.Secondary);
_downloadSpellIconPackageButton.AutoSize = false;
_downloadSpellIconPackageButton.Size = new Size(140, settingsActionButtonHeight);
_downloadSpellIconPackageButton.Dock = DockStyle.Left;
_downloadSpellIconPackageButton.Margin = new Padding(0);
_downloadSpellIconPackageButton.Click += (_, _) => StartSpellIconPackageDownload();
spellIconPackageCard.Controls.Add(_downloadSpellIconPackageButton, 0, 3);
panel.Controls.Add(spellIconPackageCard, 0, 3);
UpdateLayoutButtons();
UpdateCloseBehaviorButtons();
UpdateSpellIconPackageCard();
RefreshDefaultModuleSelector();
scrollHost.Controls.Add(panel);
scrollHost.Resize += (_, _) => panel.Width = Math.Max(0, scrollHost.ClientSize.Width - SystemInformation.VerticalScrollBarWidth);
return scrollHost;
}
private void StartSpellIconPackageDownload()
{
if (!_spellIconPackageDownloadTask.IsCompleted || _shutdownStarted)
{
return;
}
_spellIconPackageDownloadCts?.Dispose();
_spellIconPackageDownloadCts = new CancellationTokenSource();
_spellIconPackageDownloadTask = DownloadSpellIconPackageWithFeedbackAsync(
_spellIconPackageDownloadCts.Token);
}
private async Task DownloadSpellIconPackageWithFeedbackAsync(CancellationToken cancellationToken)
{
_downloadSpellIconPackageButton.Enabled = false;
var progress = new Progress<SpellIconDownloadProgress>(value =>
{
if (_shutdownStarted || _spellIconPackageStatusLabel.IsDisposed)
{
return;
}
_spellIconPackageStatusLabel.Text = value.Message;
_settingsToolTip.SetToolTip(_spellIconPackageStatusLabel, value.Message);
_downloadSpellIconPackageButton.Text = value.Percentage is { } percentage
? $"正在下载 {percentage}%"
: "正在检查……";
});
AppendLog("开始检查 GitHub 技能/物品图标数据包");
try
{
var result = await _spellIconPackageDownloadService.UpdateAsync(progress, cancellationToken);
if (_shutdownStarted)
{
return;
}
var sizeText = $"{result.Size / 1024d / 1024d:F2} MiB";
var hashText = result.Sha256[..Math.Min(12, result.Sha256.Length)];
var kind = SpellIconCatalog.IsItemDatabaseAvailable ? "完整包" : "仅技能旧包";
if (result.UpToDate)
{
_spellIconPackageStatusLabel.Text = $"已是最新({kind}{sizeText}SHA-256 {hashText}…";
AppendLog("技能/物品图标数据包已是最新,本地文件未修改");
}
else
{
_spellIconPackageStatusLabel.Text = $"安装完成({kind}{sizeText}SHA-256 {hashText}…";
AppendLog("技能/物品图标数据包已下载、校验并热加载");
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
if (!_shutdownStarted)
{
_spellIconPackageStatusLabel.Text = "下载已取消;原数据包未修改。";
AppendLog("技能/物品图标数据包下载已取消");
}
}
catch (Exception ex)
{
if (!_shutdownStarted)
{
var releaseUrl = SpellIconPackageDownloadService.ReleasesPageUrl;
var status = $"下载失败,请到 {releaseUrl} 手动下载 SpellIcons.shgpack。";
_spellIconPackageStatusLabel.Text = status;
_settingsToolTip.SetToolTip(
_spellIconPackageStatusLabel,
status + Environment.NewLine + Environment.NewLine + ex);
AppendLog($"技能/物品图标数据包下载失败: {ex.Message}");
var openPage = MessageBox.Show(
this,
"自动下载失败。请到以下页面手动下载 SpellIcons.shgpack放到程序目录的 data 文件夹:"
+ Environment.NewLine
+ Environment.NewLine
+ releaseUrl
+ Environment.NewLine
+ Environment.NewLine
+ $"保存位置:{SpellIconCatalog.PackagePath}"
+ Environment.NewLine
+ Environment.NewLine
+ $"原因:{ex.Message}"
+ Environment.NewLine
+ Environment.NewLine
+ "是否现在打开下载页面?",
"下载数据包失败",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (openPage == DialogResult.Yes)
{
OpenSpellIconReleasePage();
}
}
}
finally
{
if (!_shutdownStarted && !_downloadSpellIconPackageButton.IsDisposed)
{
_downloadSpellIconPackageButton.Enabled = true;
_downloadSpellIconPackageButton.Text = SpellIconCatalog.IsPackageAvailable
? "检查更新"
: "下载数据包";
}
}
}
private void UpdateSpellIconPackageCard()
{
var packagePath = SpellIconCatalog.PackagePath;
if (SpellIconCatalog.IsPackageAvailable && File.Exists(packagePath))
{
var length = new FileInfo(packagePath).Length;
var sizeText = $"{length / 1024d / 1024d:F2} MiB";
_spellIconPackageStatusLabel.Text = SpellIconCatalog.IsItemDatabaseAvailable
? $"已安装完整包:{sizeText}。点击检查 GitHub 更新。"
: $"已安装仅技能旧包:{sizeText}。物品搜索库不可用,可检查更新以获取完整包。";
_downloadSpellIconPackageButton.Text = "检查更新";
}
else if (File.Exists(packagePath))
{
_spellIconPackageStatusLabel.Text = "本地数据包损坏或格式不受支持;技能/物品图标与添加联想不可用。";
_downloadSpellIconPackageButton.Text = "重新下载";
}
else
{
_spellIconPackageStatusLabel.Text = "未安装;技能/物品图标与添加技能、物品联想不可用。";
_downloadSpellIconPackageButton.Text = "下载数据包";
}
_downloadSpellIconPackageButton.Enabled = true;
_settingsToolTip.SetToolTip(
_spellIconPackageStatusLabel,
_spellIconPackageStatusLabel.Text);
}
private void OpenModuleWebsite()
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(ModuleWebsiteUrl)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show(
this,
$"无法打开模块网站:{ex.Message}",
"Shigure",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private void OpenSpellIconReleasePage()
{
var releaseUrl = SpellIconPackageDownloadService.ReleasesPageUrl;
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(releaseUrl)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show(
this,
$"无法打开下载页面:{ex.Message}"
+ Environment.NewLine
+ Environment.NewLine
+ releaseUrl,
"Shigure",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private void OpenModuleDirectory()
{
var moduleDirectory = _moduleStore.ModuleDirectory;
try
{
Directory.CreateDirectory(moduleDirectory);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"\"{moduleDirectory}\"",
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show(
this,
$"无法打开模块目录:{ex.Message}",
"Shigure",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private async Task UpdateConfigFromProjectWithFeedbackAsync()
{
_updateConfigButton.Enabled = false;
_updateConfigButton.Text = "更新中…";
_configSourceLabel.ForeColor = UiTheme.Warning;
_configSourceLabel.Text = "正在生成配置并同步游戏插件…";
try
{
var updated = await UpdateConfigFromProjectAsync();
_configSourceLabel.ForeColor = updated ? UiTheme.Success : UiTheme.Danger;
}
catch
{
_configSourceLabel.ForeColor = UiTheme.Danger;
throw;
}
finally
{
_updateConfigButton.Text = "更新配置";
_updateConfigButton.Enabled = true;
_settingsToolTip.SetToolTip(_configSourceLabel, _configSourceLabel.Text);
}
}
private async Task SynchronizeAddonAtStartupAsync()
{
try
{
var result = await Task.Run(_addonSyncService.SynchronizeAll);
LogAddonSyncResult("启动插件同步", result);
}
catch (Exception ex)
{
AppendLog($"启动插件同步失败,程序将继续运行: {ex.Message}");
}
}
private async Task<bool> UpdateConfigFromProjectAsync()
{
try
{
var result = await QueueProjectConfigUpdateAsync(savedAddonFilePath: null);
if (!_shutdownStarted)
{
ShowProjectConfigUpdateResult(result);
}
return true;
}
catch (Exception ex)
{
if (_shutdownStarted)
{
return false;
}
AppendLog($"更新配置失败: {ex.Message}");
MessageBox.Show(ex.Message, "更新配置失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
_configSourceLabel.Text = $"更新失败:{ex.Message}";
return false;
}
}
private async Task<ClassConfigPostSaveResult> UpdateClassConfigAfterSaveAsync(
string savedAddonFilePath,
int classId)
{
var moduleResult = SaveModulesForClass(classId);
var result = await QueueProjectConfigUpdateAsync(savedAddonFilePath);
if (moduleResult.Errors.Count > 0)
{
throw new InvalidOperationException(
$"该职业有 {moduleResult.Errors.Count} 个模块保存失败;"
+ $"已成功保存 {moduleResult.SavedCount} 个模块。详情见日志。");
}
return new ClassConfigPostSaveResult(
DescribeAddonSyncIssue(result.AddonSync),
moduleResult.SavedCount,
moduleResult.Warnings);
}
private ClassModuleSaveResult SaveModulesForClass(int classId)
{
var modules = _moduleStore.GetModulesForDisplay()
.Where(module => module.Match.ClassId == classId)
.ToList();
var warnings = new List<string>();
var errors = new List<string>();
var savedCount = 0;
foreach (var module in modules)
{
try
{
var warning = _moduleDependencyService.Capture(module);
_moduleStore.SaveDependenciesInPlace(module);
savedCount++;
if (!string.IsNullOrWhiteSpace(warning))
{
warnings.Add($"{module.Name}: {warning}");
}
}
catch (Exception ex)
{
errors.Add($"{module.Name}: {ex.Message}");
}
}
var className = ClassNames.GetClassAndSpecName(classId, null).ClassName ?? $"职业{classId}";
AppendLog($"已同步保存 {savedCount}/{modules.Count} 个{className}模块。");
foreach (var warning in warnings)
{
AppendLog($"模块依赖保存提示: {warning}");
}
foreach (var error in errors)
{
AppendLog($"模块依赖保存失败: {error}");
}
RefreshModuleSelector(_lastSnapshot, forceRefresh: true);
RefreshDefaultModuleSelector();
return new ClassModuleSaveResult(savedCount, warnings, errors);
}
private Task<ProjectConfigUpdateResult> QueueProjectConfigUpdateAsync(string? savedAddonFilePath)
{
lock (_configUpdateSync)
{
if (_shutdownStarted)
{
return Task.FromException<ProjectConfigUpdateResult>(
new OperationCanceledException("程序正在关闭。"));
}
var updateTask = RunQueuedConfigUpdateAsync(_configUpdateTail, savedAddonFilePath);
_configUpdateTail = updateTask;
return updateTask;
}
}
private async Task<ProjectConfigUpdateResult> RunQueuedConfigUpdateAsync(
Task previousUpdate,
string? savedAddonFilePath)
{
await Task.Yield();
try
{
await previousUpdate;
}
catch
{
// 前一个调用方会收到自己的异常;队列仍继续处理后续更新。
}
if (_shutdownStarted)
{
throw new OperationCanceledException("程序正在关闭。");
}
return await UpdateConfigFromProjectCoreAsync(savedAddonFilePath);
}
private Task GetPendingConfigUpdateTask()
{
lock (_configUpdateSync)
{
return _configUpdateTail;
}
}
private async Task WaitForPendingConfigUpdatesAsync()
{
while (true)
{
var pending = GetPendingConfigUpdateTask();
await pending;
lock (_configUpdateSync)
{
if (ReferenceEquals(pending, _configUpdateTail))
{
return;
}
}
}
}
private async Task<ProjectConfigUpdateResult> UpdateConfigFromProjectCoreAsync(string? savedAddonFilePath)
{
if (_shutdownStarted)
{
throw new OperationCanceledException("程序正在关闭。");
}
var classDirectory = Path.Combine(_addonSyncService.SourceRoot, "class");
var classMacrosPath = Path.Combine(_addonSyncService.SourceRoot, "core", "classmacros.lua");
if (!Directory.Exists(classDirectory))
{
throw new DirectoryNotFoundException($"找不到项目 Fuyutsui class 目录: {classDirectory}");
}
_configSourceLabel.Text = File.Exists(classMacrosPath)
? $"项目 Fuyutsui: {classDirectory} + classmacros.lua"
: $"项目 Fuyutsui class: {classDirectory}";
var configDirectory = Path.Combine(_baseDirectory, ConfigService.ConfigDirectoryName);
var keymapDirectory = Path.Combine(_baseDirectory, "keymap");
Directory.CreateDirectory(keymapDirectory);
try
{
UseWaitCursor = true;
var result = await Task.Run(() =>
{
var configResult = FuyutsuiConfigConverter.UpdateFromClassDirectory(classDirectory, configDirectory);
FuyutsuiKeymapConverter.UpdateResult? keymapResult = null;
if (File.Exists(classMacrosPath))
{
keymapResult = FuyutsuiKeymapConverter.UpdateFromClassMacros(classMacrosPath, keymapDirectory);
}
var addonSync = string.IsNullOrWhiteSpace(savedAddonFilePath)
? _addonSyncService.SynchronizeAll()
: _addonSyncService.SynchronizeFile(savedAddonFilePath);
return new ProjectConfigUpdateResult(configResult, keymapResult, addonSync);
});
if (_shutdownStarted)
{
throw new OperationCanceledException("程序正在关闭。");
}
_moduleEditor.ReloadCatalogs();
AppendLog($"已从项目 Fuyutsui 更新配置: {result.Config.UpdatedFiles.Count} 个文件 ← {result.Config.ClassDirectory}");
foreach (var warning in result.Config.Warnings.Take(20))
{
AppendLog($"配置警告: {warning}");
}
if (result.Keymap is { } keymap)
{
AppendLog($"已从 classmacros 更新 keymap: {keymap.UpdatedFiles.Count} 个文件 ← {keymap.ClassMacrosPath}");
foreach (var warning in keymap.Warnings.Take(20))
{
AppendLog($"keymap 警告: {warning}");
}
}
else
{
AppendLog("项目 Fuyutsui 中未找到 core\\classmacros.lua已跳过 keymap 更新");
}
LogAddonSyncResult(
string.IsNullOrWhiteSpace(savedAddonFilePath) ? "游戏插件全量同步" : "游戏插件文件同步",
result.AddonSync);
if (_runtimeSession.HasSession)
{
AppendLog("配置已更新, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: true, waitForConfigUpdates: false);
}
if (_shutdownStarted)
{
throw new OperationCanceledException("程序正在关闭。");
}
return result;
}
finally
{
UseWaitCursor = false;
}
}
private void ShowProjectConfigUpdateResult(ProjectConfigUpdateResult result)
{
var warningCount = result.Config.Warnings.Count + (result.Keymap?.Warnings.Count ?? 0);
var warningText = warningCount == 0
? string.Empty
: $"\n转换警告 {warningCount} 条(详见日志)。";
var keymapText = result.Keymap is { } keymap
? $"\nkeymap: {keymap.UpdatedFiles.Count} 个文件"
: "\nkeymap: 未更新(缺少 classmacros.lua";
var syncIssue = DescribeAddonSyncIssue(result.AddonSync);
var syncText = syncIssue is null
? $"\n游戏插件: 已复制 {result.AddonSync.CopiedFiles.Count},哈希相同 {result.AddonSync.SkippedFiles.Count}\n{result.AddonSync.TargetRoot}"
: $"\n游戏插件: {syncIssue}";
_configSourceLabel.Text = syncIssue is null && warningCount == 0
? $"已更新 {result.Config.UpdatedFiles.Count} 个配置文件,并完成游戏同步"
: $"配置已更新;{syncIssue ?? $" {warningCount} "}";
_configSourceLabel.ForeColor = syncIssue is null && warningCount == 0
? UiTheme.Success
: UiTheme.Warning;
_settingsToolTip.SetToolTip(_configSourceLabel, _configSourceLabel.Text);
if (syncIssue is not null || warningCount > 0)
{
MessageBox.Show(
$"已从项目 Fuyutsui 更新 {result.Config.UpdatedFiles.Count} 个职业配置。{keymapText}{syncText}{warningText}",
"更新配置",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private void LogAddonSyncResult(string operation, FuyutsuiAddonSyncResult result)
{
if (!result.TargetFound)
{
AppendLog($"{operation}: {result.SkippedReason}");
return;
}
AppendLog(
$"{operation}: 已复制 {result.CopiedFiles.Count},哈希相同 {result.SkippedFiles.Count} → {result.TargetRoot}");
foreach (var failure in result.Failures.Take(20))
{
AppendLog($"插件同步失败: {failure.RelativePath}: {failure.Message}");
}
if (result.Failures.Count > 20)
{
AppendLog($"插件同步另有 {result.Failures.Count - 20} 个失败文件未展开。");
}
}
private static string? DescribeAddonSyncIssue(FuyutsuiAddonSyncResult result)
{
if (!result.TargetFound)
{
return result.SkippedReason;
}
if (result.Failures.Count == 0)
{
return null;
}
var first = result.Failures[0];
return result.Failures.Count == 1
? $"{first.RelativePath}: {first.Message}"
: $"{result.Failures.Count} 个文件同步失败;首个失败为 {first.RelativePath}: {first.Message}";
}
private void ApplyInitialOptions()
{
var cachedToggleKey = _uiCache.ToggleKey?.Trim();
var initialToggleKey = !string.IsNullOrWhiteSpace(cachedToggleKey)
? cachedToggleKey
: _initialOptions.ToggleKey.Trim();
initialToggleKey = string.IsNullOrWhiteSpace(initialToggleKey) ? "XBUTTON2" : initialToggleKey;
_toggleKeyName = IsUnsupportedToggleKey(initialToggleKey) ? "XBUTTON2" : initialToggleKey;
_selectedModuleId = string.IsNullOrWhiteSpace(_uiCache.SelectedModuleId)
? null
: _uiCache.SelectedModuleId.Trim();
SetToggleKeyButtonText();
_modeComboBox.SelectedIndex = _initialOptions.Mode switch
{
SendMode.Click => 1,
SendMode.Hold => 2,
_ => 0
};
RefreshModuleSelector(_lastSnapshot, forceRefresh: false);
}
private void WireSettingEvents()
{
_modeComboBox.SelectedIndexChanged += HandleSettingCommitted;
_moduleComboBox.SelectedIndexChanged += HandleModuleSelectionChanged;
}
private async void HandleSettingCommitted(object? sender, EventArgs e)
{
await RestartRuntimeAfterSettingChangeAsync();
}
private async Task StartRuntimeAsync()
{
if (_runtimeSession.IsRunning)
{
return;
}
await StartOrRestartRuntimeAsync(restart: false);
}
private async Task<bool> StartOrRestartRuntimeAsync(
bool restart,
bool waitForConfigUpdates = true)
{
if (_shutdownStarted)
{
return false;
}
var options = BuildOptions();
if (!ValidateRuntimeOptions(options))
{
return false;
}
var requestVersion = Interlocked.Increment(ref _runtimeRequestVersion);
try
{
if (waitForConfigUpdates)
{
await WaitForPendingConfigUpdatesAsync();
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
}
if (restart)
{
await _runtimeSession.RestartAsync(options, requestVersion);
}
else
{
await _runtimeSession.StartAsync(options, requestVersion);
}
}
catch (Exception ex)
{
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
var operation = restart ? "重启" : "启动";
MessageBox.Show(ex.Message, $"{operation}失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendLog($"{operation}失败: {ex.Message}");
SetRuntimeControls(running: _runtimeSession.IsRunning);
return false;
}
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
if (!_runtimeSession.IsRunning)
{
SetRuntimeControls(running: false);
return false;
}
ResetRuntimeLogState();
SetRuntimeControls(running: true);
AppendLog($"运行已{(restart ? "" : "")}: {_processLocator.DescribeConfiguredProcesses()} / {options.ToggleKey} / {ModeLabel(options.Mode)}");
return true;
}
private bool ValidateRuntimeOptions(AppOptions options)
{
if (IsUnsupportedToggleKey(options.ToggleKey))
{
MessageBox.Show("触发键不支持 ALT请选择其他按键。", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
if (_triggerKeyState.ResolveVirtualKey(options.ToggleKey) is null)
{
MessageBox.Show($"无法识别触发键: {options.ToggleKey}", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
private void ResetRuntimeLogState()
{
_lastLoggedStep = null;
_lastLoggedStepDetails = null;
_lastLoggedScanFailureReason = null;
_lastLoggedClass = null;
_lastLoggedModule = null;
_lastLoggedEnabled = null;
}
private async Task RestartRuntimeFromEditorAsync()
{
RefreshModuleSelector(_lastSnapshot, forceRefresh: false);
RefreshDefaultModuleSelector();
if (!_runtimeSession.HasSession)
{
return;
}
AppendLog("模块已变更, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: true);
}
private void ToggleEnabled()
{
if (!_runtimeSession.IsRunning)
{
return;
}
_runtimeSession.ToggleEnabled();
}
private AppOptions BuildOptions()
{
var toggleKey = string.IsNullOrWhiteSpace(_toggleKeyName)
? "XBUTTON2"
: _toggleKeyName.Trim();
return _initialOptions with { ToggleKey = toggleKey, Mode = ReadMode(), ModuleId = _selectedModuleId };
}
private SendMode ReadMode()
{
return _modeComboBox.SelectedIndex switch
{
1 => SendMode.Click,
2 => SendMode.Hold,
_ => SendMode.Switch
};
}
private void HandleSnapshotUpdated(long sessionId, RenderSnapshot snapshot)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId == sessionId)
{
ApplySnapshot(snapshot);
}
});
}
private void HandleRuntimeFailed(long sessionId, Exception exception)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId != sessionId)
{
return;
}
AppendLog($"运行异常: {exception.Message}");
foreach (var titleLabel in _titleLabels)
{
titleLabel.ForeColor = UiTheme.Danger;
}
SetRuntimeControls(running: false);
});
}
private void HandleRuntimeStopped(long sessionId)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId == sessionId)
{
SetRuntimeControls(running: false);
}
});
}
private void ApplySnapshot(RenderSnapshot snapshot)
{
_lastSnapshot = snapshot;
UpdateHeaderIconColor(snapshot.ClassId);
UpdateLogicStatusLabel(snapshot.Enabled);
foreach (var enableButton in _enableButtons)
{
enableButton.Text = enableButton == _verticalEnableButton
? snapshot.Enabled ? "关\r\n闭" : "开\r\n启"
: snapshot.Enabled ? "关闭" : "开启";
}
UpdateTrayToggleMenuItem(running: true);
RefreshModuleSelector(snapshot, forceRefresh: false);
_statusForm.ApplySnapshot(snapshot);
WriteSnapshotLog(snapshot);
}
private void RefreshModuleSelector(RenderSnapshot? snapshot, bool forceRefresh)
{
if (_moduleComboBox is null)
{
return;
}
var hasValidState = snapshot?.State?.GetBool("有效性") == true;
var (classId, specId, partyType, heroTalent, filterText) = GetModuleFilter(snapshot, hasValidState);
var modules = !hasValidState
? _moduleStore.GetModules()
: _moduleStore.FindMatches(classId, specId, partyType, heroTalent);
var signature = BuildModuleSelectorSignature(
hasValidState,
classId,
specId,
partyType,
heroTalent,
modules);
if (!forceRefresh && signature == _lastModuleSelectorSignature)
{
return;
}
_lastModuleSelectorSignature = signature;
_suppressModuleSelectionChanged = true;
try
{
_moduleComboBox.BeginUpdate();
try
{
_moduleComboBox.Items.Clear();
_moduleComboBox.Items.Add(ModuleSelectionOption.Auto);
foreach (var module in modules)
{
_moduleComboBox.Items.Add(new ModuleSelectionOption(module.Id, ModuleDisplay.FormatListItem(module)));
}
var selectedIndex = 0;
var selectedModuleVisible = string.IsNullOrWhiteSpace(_selectedModuleId);
if (!string.IsNullOrWhiteSpace(_selectedModuleId))
{
for (var i = 1; i < _moduleComboBox.Items.Count; i++)
{
if (_moduleComboBox.Items[i] is ModuleSelectionOption option
&& string.Equals(option.ModuleId, _selectedModuleId, StringComparison.OrdinalIgnoreCase))
{
selectedIndex = i;
selectedModuleVisible = true;
break;
}
}
}
_moduleComboBox.SelectedIndex = selectedIndex;
_moduleCountLabel.Text = selectedModuleVisible
? $"可选模块: {modules.Count}"
: $"可选模块: {modules.Count},已选模块不符合当前筛选";
}
finally
{
_moduleComboBox.EndUpdate();
}
}
finally
{
_suppressModuleSelectionChanged = false;
}
_moduleFilterLabel.Text = filterText;
}
private string BuildModuleSelectorSignature(
bool hasValidState,
int? classId,
int? specId,
int? partyType,
int? heroTalent,
IReadOnlyList<ModuleDefinition> modules)
{
var moduleText = string.Join("|", modules.Select(module => $"{module.Id}:{module.Name}:{ModuleDisplay.FormatMatch(module.Match)}"));
return $"{hasValidState}:{classId}:{specId}:{partyType}:{heroTalent}:{_selectedModuleId}:{moduleText}";
}
private static (int? ClassId, int? SpecId, int? PartyType, int? HeroTalent, string Text) GetModuleFilter(
RenderSnapshot? snapshot,
bool hasValidState)
{
if (!hasValidState || snapshot?.State is null)
{
return (null, null, null, null, "筛选: 等待游戏状态,暂时显示全部模块");
}
var partyType = snapshot.State.GetInt("队伍类型");
var heroTalent = snapshot.State.GetInt("英雄天赋");
return (
snapshot.ClassId,
snapshot.SpecId,
partyType,
heroTalent,
$"筛选: {ModuleDisplay.FormatState(snapshot)}");
}
private async void HandleModuleSelectionChanged(object? sender, EventArgs e)
{
if (_suppressModuleSelectionChanged)
{
return;
}
_selectedModuleId = _moduleComboBox.SelectedItem is ModuleSelectionOption option
? option.ModuleId
: null;
SaveUiCache();
AppendLog($"模块选择: {(_selectedModuleId is null ? "" : _moduleComboBox.Text)}");
await RestartRuntimeAfterSettingChangeAsync();
}
private UiDropDown CreateDefaultFilterComboBox()
{
var comboBox = new UiDropDown();
UiTheme.StyleComboBox(comboBox);
comboBox.Dock = DockStyle.Fill;
comboBox.Margin = new Padding(0, 4, 8, 4);
return comboBox;
}
private void ResetDefaultClassOptions()
{
_defaultClassComboBox.Items.Clear();
_defaultClassComboBox.Items.Add(new DefaultFilterOption("职业:任意", null));
foreach (var item in ClassNames.GetClasses())
{
_defaultClassComboBox.Items.Add(new DefaultFilterOption($"职业:{item.Name}", item.Id));
}
_defaultClassComboBox.SelectedIndex = 0;
}
private void ResetDefaultSpecOptions(int? classId)
{
_defaultSpecComboBox.Items.Clear();
_defaultSpecComboBox.Items.Add(new DefaultFilterOption("专精:任意", null));
if (classId is not null)
{
foreach (var item in ClassNames.GetSpecs(classId.Value))
{
_defaultSpecComboBox.Items.Add(new DefaultFilterOption($"专精:{item.Name}", item.Id));
}
}
_defaultSpecComboBox.SelectedIndex = 0;
}
private void ResetDefaultHeroTalentOptions(int? classId, int? specId)
{
_defaultHeroTalentComboBox.Items.Clear();
_defaultHeroTalentComboBox.Items.Add(new DefaultFilterOption("英雄天赋:任意", null));
if (classId is not null && specId is not null)
{
foreach (var item in ClassNames.GetHeroTalents(classId.Value, specId.Value))
{
_defaultHeroTalentComboBox.Items.Add(new DefaultFilterOption($"英雄天赋:{item.Name}", item.Id));
}
}
_defaultHeroTalentComboBox.SelectedIndex = 0;
}
private void ResetDefaultPartyTypeOptions()
{
_defaultPartyTypeComboBox.Items.Clear();
_defaultPartyTypeComboBox.Items.AddRange(
[
new DefaultPartyTypeOption("队伍类型:任意", null),
new DefaultPartyTypeOption("队伍类型:单人", "0"),
new DefaultPartyTypeOption("队伍类型:团队", "1-40"),
new DefaultPartyTypeOption("队伍类型:队伍", "46")
]);
_defaultPartyTypeComboBox.SelectedIndex = 0;
}
private void RefreshDefaultModuleSelector()
{
if (_defaultModuleComboBox is null)
{
return;
}
var classId = ReadDefaultFilterValue(_defaultClassComboBox);
var specId = ReadDefaultFilterValue(_defaultSpecComboBox);
var heroTalent = ReadDefaultFilterValue(_defaultHeroTalentComboBox);
var partyType = ReadDefaultPartyTypeValue();
var currentDefault = (_uiCache.DefaultModules ?? [])
.LastOrDefault(selection => selection.HasSameFilter(classId, specId, partyType, heroTalent));
var modules = _moduleStore.GetModules()
.Where(module => ModuleMatchesDefaultFilter(module.Match, classId, specId, partyType, heroTalent))
.ToList();
_defaultModuleComboBox.BeginUpdate();
try
{
_defaultModuleComboBox.Items.Clear();
foreach (var module in modules)
{
var isCurrentDefault = string.Equals(
module.Id,
currentDefault?.ModuleId,
StringComparison.OrdinalIgnoreCase);
var text = ModuleDisplay.FormatListItem(module)
+ (isCurrentDefault ? "(当前默认)" : string.Empty);
_defaultModuleComboBox.Items.Add(new DefaultModuleOption(module.Id, text));
}
if (_defaultModuleComboBox.Items.Count == 0)
{
_defaultModuleComboBox.Items.Add(DefaultModuleOption.Empty);
_defaultModuleComboBox.SelectedIndex = 0;
_setDefaultModuleButton.Enabled = false;
return;
}
var selectedIndex = 0;
if (currentDefault is not null)
{
for (var i = 0; i < _defaultModuleComboBox.Items.Count; i++)
{
if (_defaultModuleComboBox.Items[i] is DefaultModuleOption option
&& string.Equals(option.ModuleId, currentDefault.ModuleId, StringComparison.OrdinalIgnoreCase))
{
selectedIndex = i;
break;
}
}
}
_defaultModuleComboBox.SelectedIndex = selectedIndex;
_setDefaultModuleButton.Enabled = true;
}
finally
{
_defaultModuleComboBox.EndUpdate();
}
}
private static bool ModuleMatchesDefaultFilter(
ModuleMatch match,
int? classId,
int? specId,
string? partyType,
int? heroTalent)
{
if (classId is not null && match.ClassId is not null && match.ClassId != classId)
{
return false;
}
if (specId is not null && match.SpecId is not null && match.SpecId != specId)
{
return false;
}
if (heroTalent is not null && match.HeroTalent is not null && match.HeroTalent != heroTalent)
{
return false;
}
var normalizedFilter = ModuleMatch.NormalizePartyTypeValue(partyType);
var normalizedMatch = ModuleMatch.NormalizePartyTypeValue(match.PartyType);
return normalizedFilter is null
|| normalizedMatch is null
|| string.Equals(normalizedFilter, normalizedMatch, StringComparison.OrdinalIgnoreCase);
}
private async void HandleSetDefaultModuleClick(object? sender, EventArgs e)
{
if (_defaultModuleComboBox.SelectedItem is not DefaultModuleOption { ModuleId: not null } option)
{
return;
}
var classId = ReadDefaultFilterValue(_defaultClassComboBox);
var specId = ReadDefaultFilterValue(_defaultSpecComboBox);
var heroTalent = ReadDefaultFilterValue(_defaultHeroTalentComboBox);
var partyType = ReadDefaultPartyTypeValue();
_uiCache.DefaultModules ??= [];
_uiCache.DefaultModules.RemoveAll(
selection => selection.HasSameFilter(classId, specId, partyType, heroTalent));
_uiCache.DefaultModules.Add(new DefaultModuleSelection
{
ClassId = classId,
SpecId = specId,
HeroTalent = heroTalent,
PartyType = partyType,
ModuleId = option.ModuleId
});
SaveUiCache();
RefreshDefaultModuleSelector();
AppendLog($"默认模块: {option.Text}");
if (_runtimeSession.HasSession)
{
AppendLog("默认模块已变更, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: true);
}
}
private static int? ReadDefaultFilterValue(UiDropDown comboBox)
=> comboBox.SelectedItem is DefaultFilterOption option ? option.Value : null;
private string? ReadDefaultPartyTypeValue()
=> _defaultPartyTypeComboBox.SelectedItem is DefaultPartyTypeOption option ? option.Value : null;
private async Task RestartRuntimeAfterSettingChangeAsync()
{
var options = BuildOptions();
if (_runtimeSession.IsRunning && options == _runtimeSession.CurrentOptions)
{
return;
}
AppendLog("设置已变更, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: _runtimeSession.HasSession);
}
private void WriteSnapshotLog(RenderSnapshot snapshot)
{
if (!string.Equals(snapshot.ScanFailureReason, _lastLoggedScanFailureReason, StringComparison.Ordinal))
{
if (string.IsNullOrWhiteSpace(snapshot.ScanFailureReason))
{
if (!string.IsNullOrWhiteSpace(_lastLoggedScanFailureReason))
{
AppendLog("扫描已恢复");
}
}
else
{
AppendLog($"扫描失败: {snapshot.ScanFailureReason}");
}
_lastLoggedScanFailureReason = snapshot.ScanFailureReason;
}
var classSpec = snapshot.ClassName is null ? null : $"{snapshot.ClassName} / {snapshot.SpecName ?? "-"}";
if (!string.IsNullOrWhiteSpace(classSpec) && classSpec != _lastLoggedClass)
{
_lastLoggedClass = classSpec;
AppendLog($"识别职业: {classSpec}");
}
if (_lastLoggedEnabled != snapshot.Enabled)
{
_lastLoggedEnabled = snapshot.Enabled;
AppendLog(snapshot.Enabled ? "逻辑已开启" : "逻辑已关闭");
}
if (snapshot.ModuleName != _lastLoggedModule)
{
_lastLoggedModule = snapshot.ModuleName;
if (!string.IsNullOrWhiteSpace(snapshot.ModuleName))
{
AppendLog($"匹配模块: {snapshot.ModuleName}");
}
}
if (!string.IsNullOrWhiteSpace(snapshot.CurrentStep))
{
var details = BuildStepLogDetails(snapshot);
if (snapshot.CurrentStep != _lastLoggedStep || details != _lastLoggedStepDetails)
{
_lastLoggedStep = snapshot.CurrentStep;
_lastLoggedStepDetails = details;
AppendLog($"步骤: {snapshot.CurrentStep}{details}");
}
}
}
private static string BuildStepLogDetails(RenderSnapshot snapshot)
{
var fields = new (string Key, string Label)[]
{
("动作单位", "目标"),
("动作按键", "按键"),
("动作延迟", "动作延迟"),
("逻辑延迟", "逻辑延迟"),
("规则编号", "规则编号"),
("限流键", "限流键"),
("发送失败", "发送失败")
};
var details = new List<string>();
foreach (var (key, label) in fields)
{
if (!snapshot.UnitInfo.TryGetValue(key, out var value))
{
continue;
}
var text = UiTheme.FormatValue(value);
if (!string.IsNullOrWhiteSpace(text))
{
details.Add($"{label}: {text}");
}
}
return details.Count == 0 ? string.Empty : $"{string.Join("", details)}";
}
private void SetRuntimeControls(bool running)
{
if (!running)
{
UpdateHeaderIconColor(null);
UpdateLogicStatusLabel(enabled: false);
}
foreach (var enableButton in _enableButtons)
{
enableButton.Enabled = running;
}
UpdateTrayToggleMenuItem(running);
}
private void UpdateLogicStatusLabel(bool enabled)
{
foreach (var statusLabel in _runtimeStatusLabels)
{
statusLabel.Text = string.Empty;
}
foreach (var titleLabel in _titleLabels)
{
titleLabel.ForeColor = enabled ? UiTheme.Accent : UiTheme.Text;
}
}
private void PostToUi(Action action)
{
if (IsDisposed)
{
return;
}
if (InvokeRequired)
{
try
{
BeginInvoke(action);
}
catch (InvalidOperationException)
{
// Form is closing.
}
return;
}
action();
}
private void AppendLog(string message)
{
_statusForm.AppendLog(message);
}
private void BeginCaptureToggleKey()
{
ShowSettingsView();
if (_isCapturingToggleKey)
{
return;
}
_isCapturingToggleKey = true;
_toggleKeyButton.Text = "请按任意键...";
ActiveControl = null;
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (_isCapturingToggleKey)
{
return TryHandleCapturedKey(keyData & Keys.KeyCode);
}
return base.ProcessCmdKey(ref msg, keyData);
}
private bool TryHandleCapturedKey(Keys key)
{
if (key is Keys.Escape)
{
_isCapturingToggleKey = false;
SetToggleKeyButtonText();
AppendLog("已取消按键录入");
return true;
}
if (IsUnsupportedToggleKey(key.ToString()))
{
_toggleKeyButton.Text = "ALT 不支持";
AppendLog("触发键不支持 ALT, 请重试");
_ = ResetCaptureButtonTextAsync();
_isCapturingToggleKey = false;
return true;
}
var keyName = TryMapKeyToHotkey(key);
if (keyName is null)
{
_toggleKeyButton.Text = "不支持";
AppendLog("该按键暂不支持, 请重试");
_ = ResetCaptureButtonTextAsync();
_isCapturingToggleKey = false;
return true;
}
_isCapturingToggleKey = false;
_toggleKeyName = keyName;
SetToggleKeyButtonText();
SaveUiCache();
AppendLog($"已录入触发键: {_toggleKeyName}");
HandleSettingCommitted(this, EventArgs.Empty);
return true;
}
public bool PreFilterMessage(ref Message m)
{
if (!_isCapturingToggleKey)
{
return false;
}
const int WmXButtonDown = 0x020B;
const int WmKeyDown = 0x0100;
const int WmSysKeyDown = 0x0104;
if (m.Msg is WmKeyDown or WmSysKeyDown)
{
return TryHandleCapturedKey((Keys)(int)m.WParam);
}
if (m.Msg != WmXButtonDown)
{
return false;
}
var xButton = (((int)m.WParam) >> 16) & 0xFFFF;
var keyName = xButton switch
{
1 => "XBUTTON1",
2 => "XBUTTON2",
_ => null
};
if (keyName is null)
{
return false;
}
_isCapturingToggleKey = false;
_toggleKeyName = keyName;
SetToggleKeyButtonText();
SaveUiCache();
AppendLog($"已录入触发键: {_toggleKeyName}");
HandleSettingCommitted(this, EventArgs.Empty);
return true;
}
private void ApplyCachedWindowState()
{
var cachedLayout = ParseMainWindowLayout(_uiCache.MainWindowLayout);
SetMainWindowLayout(cachedLayout, persist: false);
_closeButtonBehavior = ParseCloseButtonBehavior(_uiCache.CloseButtonBehavior);
UpdateCloseBehaviorButtons();
var cachedBounds = GetCachedMainWindowBounds(cachedLayout) ?? _uiCache.MainWindowBounds;
if (!TryApplyCachedMainWindowBounds(cachedBounds)
&& _uiCache.MainWindowLocation is { } mainLocation)
{
var restoredBounds = new Rectangle(mainLocation.X, mainLocation.Y, Width, Height);
if (UiCacheStore.IsBoundsVisible(restoredBounds))
{
StartPosition = FormStartPosition.Manual;
Location = new Point(mainLocation.X, mainLocation.Y);
}
}
_statusForm.ApplyCachedBounds(_uiCache.SettingsWindowBounds);
_statusForm.ApplyCachedPage(_uiCache.SelectedSettingsPage);
}
private void SaveUiCache()
{
var latestCache = UiCacheStore.Load();
_uiCache.ModuleRulesGridColumns = latestCache.ModuleRulesGridColumns;
_uiCache.ColumnWidths = latestCache.ColumnWidths;
_uiCache.ConditionEditorWindowSize = latestCache.ConditionEditorWindowSize;
_uiCache.UnitEditorWindowSize = latestCache.UnitEditorWindowSize;
var currentBounds = CaptureMainWindowBounds();
_uiCache.MainWindowBounds = currentBounds;
SetCachedMainWindowBounds(_mainWindowLayout, currentBounds);
_uiCache.MainWindowLocation = new WindowLocation
{
X = Left,
Y = Top
};
if (_statusForm.HasKnownBounds)
{
_uiCache.SettingsWindowBounds = _statusForm.GetCachedBounds();
}
_uiCache.SelectedSettingsPage = _statusForm.SelectedPageKey;
_uiCache.MainWindowLayout = _mainWindowLayout.ToString();
_uiCache.CloseButtonBehavior = _closeButtonBehavior.ToString();
_uiCache.ToggleKey = _toggleKeyName;
_uiCache.SelectedModuleId = _selectedModuleId;
UiCacheStore.Save(_uiCache);
}
private void ShowSettingsView()
{
RefreshModuleSelector(_lastSnapshot, forceRefresh: false);
RefreshDefaultModuleSelector();
_statusForm.ShowSettings(_lastSnapshot);
}
private void InitializeTrayIcon()
{
_trayMenu = new ContextMenuStrip
{
BackColor = UiTheme.SurfaceRaised,
ForeColor = UiTheme.Text,
Font = new Font(Font.FontFamily, 10F, FontStyle.Regular),
Padding = new Padding(6),
ShowImageMargin = false,
ShowCheckMargin = false,
DropShadowEnabled = true,
Renderer = new TrayMenuRenderer()
};
ToolStripMenuItem CreateTrayMenuItem(string text)
{
return new ToolStripMenuItem(text)
{
AutoSize = false,
Size = new Size(156, 38),
Padding = new Padding(14, 0, 14, 0),
ForeColor = UiTheme.Text
};
}
var showMainMenuItem = CreateTrayMenuItem("主界面");
showMainMenuItem.Click += (_, _) => ShowMainWindow();
_trayToggleMenuItem = CreateTrayMenuItem("开启/关闭");
_trayToggleMenuItem.Click += (_, _) => ToggleEnabled();
var settingsMenuItem = CreateTrayMenuItem("设置");
settingsMenuItem.Click += (_, _) => ShowSettingsView();
var exitMenuItem = CreateTrayMenuItem("退出");
exitMenuItem.ForeColor = UiTheme.Danger;
exitMenuItem.Click += (_, _) => RequestExit();
_trayMenu.Items.AddRange([showMainMenuItem, _trayToggleMenuItem, settingsMenuItem, exitMenuItem]);
_trayMenu.Opening += (_, _) => UpdateTrayToggleMenuItem(_runtimeSession.IsRunning);
UiTheme.ApplyControlRoundedRegion(_trayMenu, 10);
_trayDefaultIcon = CreateTrayIcon(Color.White);
_trayEnabledIcon = CreateTrayIcon(UiTheme.Success);
_trayIcon = new NotifyIcon
{
Text = "Shigure - 已关闭",
Icon = _trayDefaultIcon ?? Icon ?? SystemIcons.Application,
ContextMenuStrip = _trayMenu,
Visible = true
};
_trayIcon.MouseClick += (_, e) =>
{
if (e.Button == MouseButtons.Left)
{
ShowMainWindow();
}
};
}
private void ShowMainWindow()
{
if (WindowState == FormWindowState.Minimized)
{
WindowState = FormWindowState.Normal;
}
if (!Visible)
{
Show();
}
BringToFront();
Activate();
}
private void MinimizeToTray()
{
CancelToggleKeyCapture();
SaveUiCache();
_statusForm.Hide();
Hide();
}
private void RequestExit()
{
if (_shutdownStarted)
{
return;
}
_exitRequested = true;
Close();
}
private void UpdateTrayToggleMenuItem(bool running)
{
if (_trayToggleMenuItem is null)
{
return;
}
var enabled = running && _lastSnapshot?.Enabled == true;
_trayToggleMenuItem.Enabled = running;
_trayToggleMenuItem.ForeColor = enabled ? UiTheme.Success : UiTheme.Text;
if (_trayIconShowsEnabled == enabled)
{
return;
}
_trayIconShowsEnabled = enabled;
_trayIcon.Icon = enabled
? _trayEnabledIcon ?? _trayDefaultIcon ?? Icon ?? SystemIcons.Application
: _trayDefaultIcon ?? Icon ?? SystemIcons.Application;
_trayIcon.Text = enabled ? "Shigure - 已开启" : "Shigure - 已关闭";
}
private Icon? CreateTrayIcon(Color color)
{
_headerIconMask ??= LoadHeaderIconMask();
if (_headerIconMask is null)
{
return null;
}
using var tintedIcon = TintHeaderIcon(_headerIconMask, color);
using var trayBitmap = new Bitmap(32, 32, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(trayBitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(tintedIcon, new Rectangle(0, 0, trayBitmap.Width, trayBitmap.Height));
}
var iconHandle = trayBitmap.GetHicon();
try
{
using var icon = Icon.FromHandle(iconHandle);
return (Icon)icon.Clone();
}
finally
{
NativeMethods.DestroyIcon(iconHandle);
}
}
private async Task ResetCaptureButtonTextAsync()
{
await Task.Delay(1000);
if (!IsDisposed)
{
PostToUi(SetToggleKeyButtonText);
}
}
private void CancelToggleKeyCapture()
{
if (!_isCapturingToggleKey)
{
return;
}
_isCapturingToggleKey = false;
SetToggleKeyButtonText();
}
private void SetToggleKeyButtonText()
{
_toggleKeyButton.Text = _toggleKeyName;
}
private nint HitTestResizeGrip(Point clientPoint)
{
var left = clientPoint.X <= ResizeGripSize;
var right = clientPoint.X >= ClientSize.Width - ResizeGripSize;
var top = clientPoint.Y <= ResizeGripSize;
var bottom = clientPoint.Y >= ClientSize.Height - ResizeGripSize;
if (top && left)
{
return NativeMethods.HtTopLeft;
}
if (top && right)
{
return NativeMethods.HtTopRight;
}
if (bottom && left)
{
return NativeMethods.HtBottomLeft;
}
if (bottom && right)
{
return NativeMethods.HtBottomRight;
}
if (left)
{
return NativeMethods.HtLeft;
}
if (right)
{
return NativeMethods.HtRight;
}
if (top)
{
return NativeMethods.HtTop;
}
if (bottom)
{
return NativeMethods.HtBottom;
}
return NativeMethods.HtClient;
}
private string? TryMapKeyToHotkey(Keys key)
{
var keyName = key.ToString().ToUpperInvariant();
if (IsUnsupportedToggleKey(keyName))
{
return null;
}
if (key is >= Keys.D0 and <= Keys.D9)
{
return ((char)('0' + (key - Keys.D0))).ToString();
}
if (key is >= Keys.NumPad0 and <= Keys.NumPad9)
{
return $"NUMPAD{key - Keys.NumPad0}";
}
return keyName switch
{
"OEMCOMMA" => ",",
"OEMPERIOD" => ".",
"OEMQUESTION" => "/",
"OEMSEMICOLON" => ";",
"OEMQUOTES" => "'",
"OEMOPENBRACKETS" => "[",
"OEMCLOSEBRACKETS" => "]",
"OEMPLUS" => "=",
"OEMMINUS" => "-",
"OEMTILDE" => "`",
"OEMBACKSLASH" => "\\",
"DECIMAL" => "NUMPADDECIMAL",
"ADD" => "NUMPADPLUS",
"SUBTRACT" => "NUMPADMINUS",
"MULTIPLY" => "NUMPADMULTIPLY",
"DIVIDE" => "NUMPADDIVIDE",
_ => _triggerKeyState.ResolveVirtualKey(keyName) is not null ? keyName : null
};
}
private static bool IsUnsupportedToggleKey(string keyName)
{
var key = keyName.Trim().ToUpperInvariant();
return key is "ALT" or "MENU" or "LMENU" or "RMENU";
}
private static Label CreateInfoLabel(string text)
{
return new Label
{
Text = text,
AutoSize = true,
ForeColor = UiTheme.Muted,
Margin = new Padding(0, 0, 0, 4)
};
}
private void EnableDrag(Control control)
{
control.MouseDown += (_, e) =>
{
if (e.Button == MouseButtons.Left)
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessageW(Handle, NativeMethods.WmNcLButtonDown, NativeMethods.HtCaption, 0);
}
};
}
private static void ConfigureTopBarButton(Button button)
{
button.AutoSize = false;
button.Size = new Size(88, 36);
button.Padding = new Padding(4, 1, 4, 1);
}
private static Button CreateTopBarButton(string text, Color backColor, Color foreColor, bool vertical)
{
Button button;
if (vertical)
{
var stackedButton = new StackedTextButton();
UiTheme.StyleButton(stackedButton, text, backColor, foreColor);
button = stackedButton;
}
else
{
button = UiTheme.CreateButton(text, backColor, foreColor);
}
ConfigureTopBarButton(button);
button.Size = vertical ? new Size(36, 88) : new Size(88, 36);
button.Margin = vertical ? new Padding(0, 6, 0, 0) : new Padding(6, 0, 0, 0);
return button;
}
private void SetMainWindowLayout(MainWindowLayout layout, bool persist = true)
{
if (_mainWindowLayout == layout)
{
UpdateLayoutButtons();
return;
}
if (persist)
{
SetCachedMainWindowBounds(_mainWindowLayout, CaptureMainWindowBounds());
}
var previousClientSize = ClientSize;
_mainWindowLayout = layout;
var vertical = layout == MainWindowLayout.Vertical;
SuspendLayout();
try
{
MinimumSize = Size.Empty;
ClientSize = new Size(previousClientSize.Height, previousClientSize.Width);
MinimumSize = vertical ? new Size(56, 420) : new Size(420, 56);
_horizontalTopBar.Visible = !vertical;
_verticalTopBar.Visible = vertical;
(vertical ? _verticalTopBar : _horizontalTopBar).BringToFront();
}
finally
{
ResumeLayout(true);
}
if (persist)
{
TryApplyCachedMainWindowBounds(GetCachedMainWindowBounds(layout));
}
UpdateLayoutButtons();
if (persist)
{
SaveUiCache();
}
}
private WindowBounds CaptureMainWindowBounds()
{
return new WindowBounds
{
X = Left,
Y = Top,
Width = Width,
Height = Height
};
}
private WindowBounds? GetCachedMainWindowBounds(MainWindowLayout layout)
=> layout == MainWindowLayout.Vertical
? _uiCache.VerticalMainWindowBounds
: _uiCache.HorizontalMainWindowBounds;
private void SetCachedMainWindowBounds(MainWindowLayout layout, WindowBounds bounds)
{
if (layout == MainWindowLayout.Vertical)
{
_uiCache.VerticalMainWindowBounds = bounds;
}
else
{
_uiCache.HorizontalMainWindowBounds = bounds;
}
}
private bool TryApplyCachedMainWindowBounds(WindowBounds? bounds)
{
if (bounds is null)
{
return false;
}
var restoredBounds = new Rectangle(
bounds.X,
bounds.Y,
Math.Max(MinimumSize.Width, bounds.Width),
Math.Max(MinimumSize.Height, bounds.Height));
if (!UiCacheStore.IsBoundsVisible(restoredBounds))
{
return false;
}
StartPosition = FormStartPosition.Manual;
Bounds = restoredBounds;
return true;
}
private static MainWindowLayout ParseMainWindowLayout(string? value)
=> Enum.TryParse<MainWindowLayout>(value, ignoreCase: true, out var layout)
? layout
: MainWindowLayout.Horizontal;
private static CloseButtonBehavior ParseCloseButtonBehavior(string? value)
=> Enum.TryParse<CloseButtonBehavior>(value, ignoreCase: true, out var behavior)
? behavior
: CloseButtonBehavior.MinimizeToTray;
private void SetCloseButtonBehavior(CloseButtonBehavior behavior)
{
_closeButtonBehavior = behavior;
UpdateCloseBehaviorButtons();
SaveUiCache();
}
private void UpdateCloseBehaviorButtons()
{
if (_minimizeToTrayButton is null || _exitOnCloseButton is null)
{
return;
}
StyleLayoutButton(_minimizeToTrayButton, _closeButtonBehavior == CloseButtonBehavior.MinimizeToTray);
StyleLayoutButton(_exitOnCloseButton, _closeButtonBehavior == CloseButtonBehavior.Exit);
}
private void UpdateLayoutButtons()
{
if (_horizontalLayoutButton is null || _verticalLayoutButton is null)
{
return;
}
StyleLayoutButton(_horizontalLayoutButton, _mainWindowLayout == MainWindowLayout.Horizontal);
StyleLayoutButton(_verticalLayoutButton, _mainWindowLayout == MainWindowLayout.Vertical);
}
private static void StyleLayoutButton(Button button, bool selected)
{
button.BackColor = selected ? UiTheme.Accent : UiTheme.Field;
button.ForeColor = selected ? Color.FromArgb(10, 31, 31) : UiTheme.Text;
button.FlatAppearance.BorderColor = selected ? UiTheme.Accent : UiTheme.Border;
button.FlatAppearance.MouseOverBackColor = selected ? Color.FromArgb(112, 234, 221) : UiTheme.Hover;
button.FlatAppearance.MouseDownBackColor = selected ? Color.FromArgb(62, 194, 181) : UiTheme.Pressed;
}
private sealed class TrayMenuRenderer : ToolStripProfessionalRenderer
{
public TrayMenuRenderer()
: base(new TrayMenuColorTable())
{
RoundedEdges = true;
}
protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
{
e.Graphics.Clear(UiTheme.SurfaceRaised);
}
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
{
var bounds = new Rectangle(0, 0, e.ToolStrip.Width - 1, e.ToolStrip.Height - 1);
using var path = UiTheme.CreateRoundedRectanglePath(bounds, 10);
using var pen = new Pen(UiTheme.Border);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.DrawPath(pen, path);
}
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
{
if (!e.Item.Selected)
{
return;
}
var bounds = new Rectangle(2, 2, e.Item.Width - 4, e.Item.Height - 4);
var background = e.Item.ForeColor == UiTheme.Danger ? UiTheme.DangerSoft : UiTheme.Hover;
using var path = UiTheme.CreateRoundedRectanglePath(bounds, 7);
using var brush = new SolidBrush(background);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.FillPath(brush, path);
}
}
private sealed class TrayMenuColorTable : ProfessionalColorTable
{
public override Color ToolStripDropDownBackground => UiTheme.SurfaceRaised;
public override Color MenuItemSelected => UiTheme.Hover;
public override Color MenuItemBorder => UiTheme.Border;
public override Color ToolStripBorder => UiTheme.Border;
public override Color ImageMarginGradientBegin => UiTheme.SurfaceRaised;
public override Color ImageMarginGradientMiddle => UiTheme.SurfaceRaised;
public override Color ImageMarginGradientEnd => UiTheme.SurfaceRaised;
}
private sealed class RotatableLabel : Label
{
private string _displayText = string.Empty;
private bool _rotated;
private bool _suppressBaseText;
[System.ComponentModel.DesignerSerializationVisibility(
System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public bool Rotated
{
get => _rotated;
set
{
if (_rotated == value)
{
return;
}
_rotated = value;
var size = Size;
Size = new Size(size.Height, size.Width);
base.Text = value ? string.Empty : _displayText;
Invalidate();
}
}
[System.Diagnostics.CodeAnalysis.AllowNull]
public override string Text
{
get => _suppressBaseText ? string.Empty : _displayText;
set
{
_displayText = value ?? string.Empty;
base.Text = _rotated ? string.Empty : _displayText;
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
if (!_rotated)
{
base.OnPaint(e);
return;
}
_suppressBaseText = true;
try
{
base.OnPaint(e);
}
finally
{
_suppressBaseText = false;
}
if (string.IsNullOrEmpty(_displayText))
{
return;
}
DrawRotatedText(e.Graphics, ClientRectangle, _displayText, Font, ForeColor);
}
}
private sealed class StackedTextButton : Button
{
private string _displayText = string.Empty;
private bool _suppressBaseText;
[System.Diagnostics.CodeAnalysis.AllowNull]
public override string Text
{
get => _suppressBaseText ? string.Empty : _displayText;
set
{
_displayText = value ?? string.Empty;
base.Text = string.Empty;
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs pevent)
{
_suppressBaseText = true;
try
{
base.OnPaint(pevent);
}
finally
{
_suppressBaseText = false;
}
var lines = _displayText.Replace("\r", string.Empty).Split('\n');
if (lines.Length == 0)
{
return;
}
var flags = TextFormatFlags.HorizontalCenter
| TextFormatFlags.VerticalCenter
| TextFormatFlags.NoPadding
| TextFormatFlags.NoPrefix;
var lineHeight = TextRenderer.MeasureText(
pevent.Graphics,
"开",
Font,
Size.Empty,
TextFormatFlags.NoPadding).Height;
var totalHeight = lineHeight * lines.Length;
var top = Math.Max(0, (ClientSize.Height - totalHeight) / 2);
for (var index = 0; index < lines.Length; index++)
{
var lineBounds = new Rectangle(0, top + (index * lineHeight), ClientSize.Width, lineHeight);
TextRenderer.DrawText(pevent.Graphics, lines[index], Font, lineBounds, ForeColor, flags);
}
}
}
private static void DrawRotatedText(
Graphics graphics,
Rectangle bounds,
string text,
Font font,
Color color)
{
var state = graphics.Save();
try
{
graphics.TranslateTransform(bounds.Left + bounds.Width / 2F, bounds.Top + bounds.Height / 2F);
graphics.RotateTransform(90F);
using var brush = new SolidBrush(color);
using var format = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap
};
graphics.DrawString(
text,
font,
brush,
new RectangleF(-bounds.Height / 2F, -bounds.Width / 2F, bounds.Height, bounds.Width),
format);
}
finally
{
graphics.Restore(state);
}
}
private sealed record ModuleSelectionOption(string? ModuleId, string Text)
{
public static readonly ModuleSelectionOption Auto = new(null, "自动选择(最匹配)");
public override string ToString()
{
return Text;
}
}
private sealed record DefaultFilterOption(string Text, int? Value)
{
public override string ToString() => Text;
}
private sealed record DefaultPartyTypeOption(string Text, string? Value)
{
public override string ToString() => Text;
}
private sealed record DefaultModuleOption(string? ModuleId, string Text)
{
public static readonly DefaultModuleOption Empty = new(null, "暂无符合筛选的模块");
public override string ToString() => Text;
}
private static Icon? LoadApplicationIcon()
{
try
{
return Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch
{
return null;
}
}
private void TryApplyApplicationIcon()
{
var icon = LoadApplicationIcon();
if (icon != null)
{
Icon = icon;
}
}
private static string ModeLabel(SendMode mode)
{
return mode switch
{
SendMode.Click => "单击",
SendMode.Hold => "按住",
_ => "开关"
};
}
}