项目文件夹

文件
2026-07-13 12:24:21 +08:00

901 行
34 KiB
C#

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// Copyright 2025 Takuto Nakamura
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using RunCat365.Properties;
using System.Diagnostics;
namespace RunCat365
{
internal class CustomRunnerForm : Form
{
private const int PREVIEW_INITIAL_INTERVAL_MS = 100;
private const int PREVIEW_MIN_INTERVAL_MS = 20;
private const int PREVIEW_BASE_RATE = 500;
private const int RUNNER_NAME_MAX_LENGTH = 30;
private readonly CustomRunnerRepository repository;
private readonly Action<string> onCustomRunnerDeleted;
private readonly string fontFamily;
private readonly Font tileLabelFont;
private readonly ToolTip toolTip = new();
private ListBox runnerListBox = null!;
private TextBox nameTextBox = null!;
private FlowLayoutPanel framePanel = null!;
private Button addFramesButton = null!;
private Button removeFrameButton = null!;
private Button saveButton = null!;
private Button deleteButton = null!;
private FrameAnimationView previewView = null!;
private FlatTrackBar speedSlider = null!;
private readonly List<Bitmap> pendingFrames = [];
private readonly List<Bitmap> recoloredFrames = [];
private int selectedFrameIndex = -1;
private Point? dragOriginPoint;
private int dragSourceIndex = -1;
internal CustomRunnerForm(
CustomRunnerRepository repository,
Action<string> onCustomRunnerDeleted
)
{
this.repository = repository;
this.onCustomRunnerDeleted = onCustomRunnerDeleted;
fontFamily = SupportedLanguageExtension.GetCurrentLanguage().GetFontName();
tileLabelFont = new Font(fontFamily, 7.5F);
InitializeFormProperties();
InitializeControls();
InitializeEventHandlers();
FitWindowToContent();
RefreshRunnerList();
ValidateFormState();
}
private void FitWindowToContent()
{
if (Controls.Count == 0) return;
var root = Controls[0];
root.PerformLayout();
var preferred = root.PreferredSize;
ClientSize = new Size(
preferred.Width + Padding.Horizontal,
preferred.Height + Padding.Vertical
);
}
private void InitializeFormProperties()
{
Text = Strings.Window_CustomRunners;
Icon = Resources.AppIcon;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
BackColor = Palette.FormBackground;
ForeColor = Palette.TextPrimary;
Padding = new Padding(20);
}
private void InitializeControls()
{
var listColumn = BuildListColumn();
var separator = BuildVerticalSeparator();
var editorColumn = BuildEditorColumn();
var root = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 3,
RowCount = 1,
BackColor = Color.Transparent
};
root.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
root.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.Controls.Add(listColumn, 0, 0);
root.Controls.Add(separator, 1, 0);
root.Controls.Add(editorColumn, 2, 0);
Controls.Add(root);
root.Location = new Point(Padding.Left, Padding.Top);
}
private TableLayoutPanel BuildListColumn()
{
var listLabel = CreateLabel(
Strings.CustomRunner_AddedRunners,
9F, FontStyle.Bold, Palette.TextSecondary
);
listLabel.Margin = new Padding(0, 0, 0, 4);
runnerListBox = new ListBox
{
MinimumSize = new Size(160, 200),
BackColor = Palette.ControlBackground,
ForeColor = Palette.TextPrimary,
Font = new Font(fontFamily, 9F),
BorderStyle = BorderStyle.FixedSingle,
IntegralHeight = false,
Dock = DockStyle.Fill,
Margin = new Padding(0, 0, 0, 8)
};
deleteButton = CreateIconColoredButton(
"×",
new Size(32, 28),
Palette.DangerRed,
Palette.DangerBorder,
Strings.CustomRunner_Delete,
initiallyEnabled: false
);
deleteButton.Anchor = AnchorStyles.Left;
deleteButton.Margin = new Padding(0);
var column = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
BackColor = Color.Transparent
};
column.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
column.RowStyles.Add(new RowStyle(SizeType.AutoSize));
column.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
column.RowStyles.Add(new RowStyle(SizeType.AutoSize));
column.Controls.Add(listLabel, 0, 0);
column.Controls.Add(runnerListBox, 0, 1);
column.Controls.Add(deleteButton, 0, 2);
return column;
}
private static Panel BuildVerticalSeparator()
{
return new Panel
{
Width = 1,
BackColor = Palette.BorderAccent,
Anchor = AnchorStyles.Top | AnchorStyles.Bottom,
Margin = new Padding(12, 0, 12, 0)
};
}
private Label CreateLabel(string text, float fontSize, FontStyle fontStyle, Color foreColor)
{
var font = new Font(fontFamily, fontSize, fontStyle);
var measured = TextRenderer.MeasureText(text, font);
return new Label
{
Text = text,
AutoSize = true,
MinimumSize = new Size(measured.Width + 2, 0),
Font = font,
ForeColor = foreColor
};
}
private TableLayoutPanel BuildEditorColumn()
{
var nameLabel = CreateLabel(
Strings.CustomRunner_Name,
9F, FontStyle.Regular, Palette.TextSecondary
);
nameLabel.Anchor = AnchorStyles.Top | AnchorStyles.Right;
nameLabel.TextAlign = ContentAlignment.MiddleRight;
nameLabel.Margin = new Padding(0, 6, 12, 8);
nameTextBox = new TextBox
{
MinimumSize = new Size(280, 24),
BackColor = Palette.ControlBackground,
ForeColor = Palette.TextPrimary,
Font = new Font(fontFamily, 9F),
BorderStyle = BorderStyle.FixedSingle,
MaxLength = RUNNER_NAME_MAX_LENGTH,
Anchor = AnchorStyles.Left | AnchorStyles.Right,
Margin = new Padding(0, 0, 0, 8)
};
var requirementsCaption = CreateLabel(
Strings.CustomRunner_RequirementsLabel,
9F, FontStyle.Regular, Palette.TextSecondary
);
requirementsCaption.Anchor = AnchorStyles.Top | AnchorStyles.Right;
requirementsCaption.TextAlign = ContentAlignment.MiddleRight;
requirementsCaption.Margin = new Padding(0, 0, 12, 8);
var requirementsContent = CreateLabel(
Strings.CustomRunner_Requirements,
9F, FontStyle.Regular, Palette.TextMuted
);
requirementsContent.Margin = new Padding(0, 0, 0, 8);
var framesCaption = CreateLabel(
Strings.CustomRunner_FramesLabel,
9F, FontStyle.Regular, Palette.TextSecondary
);
framesCaption.Anchor = AnchorStyles.Top | AnchorStyles.Right;
framesCaption.TextAlign = ContentAlignment.MiddleRight;
framesCaption.Margin = new Padding(0, 0, 12, 8);
var framesArea = BuildFramesArea();
framesArea.Margin = new Padding(0, 0, 0, 8);
framesArea.Anchor = AnchorStyles.Left | AnchorStyles.Right;
var previewCaption = CreateLabel(
Strings.CustomRunner_Preview,
9F, FontStyle.Regular, Palette.TextSecondary
);
previewCaption.Anchor = AnchorStyles.Top | AnchorStyles.Right;
previewCaption.TextAlign = ContentAlignment.MiddleRight;
previewCaption.Margin = new Padding(0, 0, 12, 8);
var previewArea = BuildPreviewArea();
previewArea.Margin = new Padding(0, 0, 0, 8);
previewArea.Anchor = AnchorStyles.Left | AnchorStyles.Right;
saveButton = CreateColoredButton(
Strings.CustomRunner_Save,
new Size(85, 30),
Palette.AccentBlue,
Palette.AccentBlueBorder,
bold: true,
initiallyEnabled: false
);
saveButton.Anchor = AnchorStyles.Right;
saveButton.Margin = new Padding(0);
var column = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 2,
RowCount = 5,
BackColor = Color.Transparent
};
column.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
column.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 0; i < 5; i++)
{
column.RowStyles.Add(new RowStyle(SizeType.AutoSize));
}
column.Controls.Add(nameLabel, 0, 0);
column.Controls.Add(nameTextBox, 1, 0);
column.Controls.Add(requirementsCaption, 0, 1);
column.Controls.Add(requirementsContent, 1, 1);
column.Controls.Add(framesCaption, 0, 2);
column.Controls.Add(framesArea, 1, 2);
column.Controls.Add(previewCaption, 0, 3);
column.Controls.Add(previewArea, 1, 3);
column.Controls.Add(saveButton, 1, 4);
return column;
}
private TableLayoutPanel BuildFramesArea()
{
framePanel = new FlowLayoutPanel
{
MinimumSize = new Size(350, 280),
AutoScroll = true,
BackColor = Palette.FramePanelBackground,
BorderStyle = BorderStyle.FixedSingle,
WrapContents = true,
AllowDrop = true,
Anchor = AnchorStyles.Left | AnchorStyles.Right,
Margin = new Padding(0, 0, 0, 4)
};
framePanel.DragEnter += FrameDragOver;
framePanel.DragOver += FrameDragOver;
framePanel.DragDrop += FramePanelDragDrop;
addFramesButton = CreateIconButton(
"+",
new Size(28, 24),
Strings.CustomRunner_AddFrames
);
addFramesButton.Margin = new Padding(0, 0, 4, 0);
removeFrameButton = CreateIconButton(
"",
new Size(28, 24),
Strings.CustomRunner_RemoveFrame,
initiallyEnabled: false
);
removeFrameButton.Margin = new Padding(0);
var buttonRow = new FlowLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Anchor = AnchorStyles.Left,
Margin = new Padding(0)
};
buttonRow.Controls.Add(addFramesButton);
buttonRow.Controls.Add(removeFrameButton);
var area = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
RowCount = 2,
BackColor = Color.Transparent
};
area.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
area.RowStyles.Add(new RowStyle(SizeType.AutoSize));
area.RowStyles.Add(new RowStyle(SizeType.AutoSize));
area.Controls.Add(framePanel, 0, 0);
area.Controls.Add(buttonRow, 0, 1);
return area;
}
private TableLayoutPanel BuildPreviewArea()
{
previewView = new FrameAnimationView
{
Size = new Size(64, 64),
BackColor = Palette.FramePanelBackground,
FrameIntervalMs = PREVIEW_INITIAL_INTERVAL_MS,
Anchor = AnchorStyles.None,
Margin = new Padding(0, 0, 12, 0)
};
var turtleIcon = new PictureBox
{
Size = new Size(24, 24),
SizeMode = PictureBoxSizeMode.Zoom,
Image = Resources.slider_turtle,
BackColor = Color.Transparent,
Anchor = AnchorStyles.None,
Margin = new Padding(0, 0, 8, 0)
};
speedSlider = new FlatTrackBar
{
Height = 24,
Minimum = 1,
Maximum = 10,
Value = 5,
Anchor = AnchorStyles.Left | AnchorStyles.Right,
Margin = new Padding(0)
};
toolTip.SetToolTip(speedSlider, Strings.CustomRunner_PreviewSpeed);
var rabbitIcon = new PictureBox
{
Size = new Size(24, 24),
SizeMode = PictureBoxSizeMode.Zoom,
Image = Resources.slider_rabbit,
BackColor = Color.Transparent,
Anchor = AnchorStyles.None,
Margin = new Padding(8, 0, 0, 0)
};
var area = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 4,
RowCount = 1,
BackColor = Color.Transparent
};
area.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
area.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
area.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
area.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
area.RowStyles.Add(new RowStyle(SizeType.AutoSize));
area.Controls.Add(previewView, 0, 0);
area.Controls.Add(turtleIcon, 1, 0);
area.Controls.Add(speedSlider, 2, 0);
area.Controls.Add(rabbitIcon, 3, 0);
return area;
}
private void InitializeEventHandlers()
{
runnerListBox.SelectedIndexChanged += RunnerListBoxSelectedIndexChanged;
nameTextBox.TextChanged += (sender, e) => ValidateFormState();
addFramesButton.Click += AddFramesButtonClick;
removeFrameButton.Click += RemoveFrameButtonClick;
speedSlider.ValueChanged += (sender, e) =>
{
if (speedSlider.Value > 0)
{
previewView.FrameIntervalMs = Math.Max(PREVIEW_MIN_INTERVAL_MS, PREVIEW_BASE_RATE / speedSlider.Value);
}
};
deleteButton.Click += DeleteButtonClick;
saveButton.Click += SaveButtonClick;
}
private ThemedButton CreateIconButton(string glyph, Size size, string tooltipText, bool initiallyEnabled = true)
{
var button = new ThemedButton
{
Text = glyph,
AutoSize = false,
Size = size,
MinimumSize = size,
Padding = new Padding(0),
FlatStyle = FlatStyle.Flat,
BackColor = Palette.ButtonBackground,
ForeColor = Palette.TextPrimary,
Font = new Font("Segoe UI", 11F, FontStyle.Regular),
Cursor = Cursors.Hand,
Enabled = initiallyEnabled
};
button.FlatAppearance.BorderColor = Palette.BorderAccent;
toolTip.SetToolTip(button, tooltipText);
return button;
}
private ThemedButton CreateIconColoredButton(
string glyph,
Size size,
Color backColor,
Color borderColor,
string tooltipText,
bool initiallyEnabled
)
{
var button = new ThemedButton
{
Text = glyph,
AutoSize = false,
Size = size,
MinimumSize = size,
Padding = new Padding(0),
FlatStyle = FlatStyle.Flat,
BackColor = backColor,
ForeColor = Palette.TextPrimary,
Font = new Font("Segoe UI", 11F, FontStyle.Regular),
Cursor = Cursors.Hand,
Enabled = initiallyEnabled
};
button.FlatAppearance.BorderColor = borderColor;
toolTip.SetToolTip(button, tooltipText);
return button;
}
private ThemedButton CreateColoredButton(
string text,
Size minimumSize,
Color backColor,
Color borderColor,
bool bold,
bool initiallyEnabled
)
{
var button = new ThemedButton
{
Text = text,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = minimumSize,
Padding = new Padding(14, 4, 14, 4),
FlatStyle = FlatStyle.Flat,
BackColor = backColor,
ForeColor = Palette.TextPrimary,
Font = new Font(fontFamily, bold ? 9F : 8.5F, bold ? FontStyle.Bold : FontStyle.Regular),
Cursor = Cursors.Hand,
Enabled = initiallyEnabled
};
button.FlatAppearance.BorderColor = borderColor;
return button;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
ClearPendingFrames();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
foreach (var frame in pendingFrames) frame.Dispose();
pendingFrames.Clear();
foreach (var frame in recoloredFrames) frame.Dispose();
recoloredFrames.Clear();
tileLabelFont.Dispose();
toolTip.Dispose();
}
base.Dispose(disposing);
}
private void RefreshRunnerList()
{
runnerListBox.Items.Clear();
foreach (var profile in repository.GetAll())
{
runnerListBox.Items.Add(profile.Name);
}
UpdateRunnerActionButtons();
}
private void RunnerListBoxSelectedIndexChanged(object? sender, EventArgs e)
{
var hasSelection = runnerListBox.SelectedIndex >= 0;
UpdateRunnerActionButtons();
if (hasSelection && runnerListBox.SelectedItem is string selectedName)
{
nameTextBox.Text = selectedName;
ClearPendingFrames();
var frames = repository.LoadFrames(selectedName);
pendingFrames.AddRange(frames);
selectedFrameIndex = pendingFrames.Count > 0 ? 0 : -1;
OnFramesChanged();
}
}
private void AddFramesButtonClick(object? sender, EventArgs e)
{
using var dialog = new OpenFileDialog
{
Filter = "PNG Images|*.png",
Multiselect = true,
Title = Strings.CustomRunner_AddFrames
};
if (dialog.ShowDialog() != DialogResult.OK) return;
var sortedFiles = dialog.FileNames.OrderBy(f => f).ToArray();
foreach (var file in sortedFiles)
{
if (pendingFrames.Count >= CustomRunnerRepository.MAX_FRAME_COUNT) break;
try
{
pendingFrames.Add(new Bitmap(file));
}
catch (Exception ex) when (ex is OutOfMemoryException or ArgumentException or FileNotFoundException)
{
Debug.WriteLine($"Failed to load frame '{file}': {ex.Message}");
}
}
OnFramesChanged();
}
private void RemoveFrameButtonClick(object? sender, EventArgs e)
{
if (selectedFrameIndex < 0 || pendingFrames.Count <= selectedFrameIndex) return;
pendingFrames[selectedFrameIndex].Dispose();
pendingFrames.RemoveAt(selectedFrameIndex);
selectedFrameIndex = pendingFrames.Count == 0
? -1
: Math.Min(selectedFrameIndex, pendingFrames.Count - 1);
OnFramesChanged();
}
private void OnFramesChanged()
{
RebuildRecoloredFrames();
RebuildFrameGrid();
RestartPreview();
ValidateFormState();
}
private void RebuildRecoloredFrames()
{
foreach (var frame in recoloredFrames) frame.Dispose();
recoloredFrames.Clear();
foreach (var frame in pendingFrames)
{
recoloredFrames.Add(frame.Recolor(Color.White));
}
if (pendingFrames.Count <= selectedFrameIndex)
{
selectedFrameIndex = pendingFrames.Count - 1;
}
}
private void RebuildFrameGrid()
{
framePanel.SuspendLayout();
foreach (Control oldTile in framePanel.Controls)
{
oldTile.Dispose();
}
framePanel.Controls.Clear();
for (int i = 0; i < recoloredFrames.Count; i++)
{
framePanel.Controls.Add(CreateFrameTile(i));
}
framePanel.ResumeLayout();
}
private Panel CreateFrameTile(int frameIndex)
{
var container = new Panel
{
Size = new Size(70, 76),
Margin = new Padding(4),
BackColor = frameIndex == selectedFrameIndex ? Palette.SelectionHighlight : Color.Transparent,
Cursor = Cursors.SizeAll
};
var pictureBox = new PictureBox
{
Size = new Size(48, 48),
Location = new Point(11, 0),
SizeMode = PictureBoxSizeMode.Zoom,
Image = recoloredFrames[frameIndex],
BackColor = Palette.FrameThumbnailBackground,
Cursor = Cursors.SizeAll
};
var indexLabel = new Label
{
Text = $"{frameIndex}",
Size = new Size(70, 18),
Location = new Point(0, 52),
TextAlign = ContentAlignment.TopCenter,
ForeColor = Palette.TextThumbnailIndex,
Font = tileLabelFont,
Cursor = Cursors.SizeAll
};
foreach (var control in new Control[] { container, pictureBox, indexLabel })
{
control.Click += (sender, e) => SelectFrame(frameIndex);
WireFrameDragSource(control, frameIndex);
WireFrameDragReceiver(control, frameIndex);
}
container.Controls.Add(pictureBox);
container.Controls.Add(indexLabel);
return container;
}
private void WireFrameDragSource(Control control, int frameIndex)
{
control.MouseDown += (sender, e) =>
{
if (e.Button != MouseButtons.Left) return;
dragOriginPoint = e.Location;
dragSourceIndex = frameIndex;
};
control.MouseMove += (sender, e) =>
{
if (dragOriginPoint is null || dragSourceIndex < 0) return;
if (e.Button != MouseButtons.Left) return;
var dx = Math.Abs(e.X - dragOriginPoint.Value.X);
var dy = Math.Abs(e.Y - dragOriginPoint.Value.Y);
if (dx < SystemInformation.DragSize.Width && dy < SystemInformation.DragSize.Height) return;
var startedIndex = dragSourceIndex;
dragOriginPoint = null;
dragSourceIndex = -1;
control.DoDragDrop(startedIndex, DragDropEffects.Move);
};
control.MouseUp += (sender, e) =>
{
dragOriginPoint = null;
dragSourceIndex = -1;
};
}
private void WireFrameDragReceiver(Control control, int frameIndex)
{
control.AllowDrop = true;
control.DragEnter += FrameDragOver;
control.DragOver += FrameDragOver;
control.DragDrop += (sender, e) => HandleFrameDrop(e, frameIndex);
}
private static void FrameDragOver(object? sender, DragEventArgs e)
{
e.Effect = e.Data?.GetDataPresent(typeof(int)) == true
? DragDropEffects.Move
: DragDropEffects.None;
}
private void HandleFrameDrop(DragEventArgs e, int targetIndex)
{
if (e.Data?.GetData(typeof(int)) is not int sourceIndex) return;
ReorderFrame(sourceIndex, targetIndex);
}
private void FramePanelDragDrop(object? sender, DragEventArgs e)
{
if (e.Data?.GetData(typeof(int)) is not int sourceIndex) return;
ReorderFrame(sourceIndex, pendingFrames.Count);
}
private void ReorderFrame(int sourceIndex, int targetIndex)
{
if (sourceIndex < 0 || sourceIndex >= pendingFrames.Count) return;
if (sourceIndex == targetIndex) return;
var item = pendingFrames[sourceIndex];
pendingFrames.RemoveAt(sourceIndex);
var insertAt = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex;
insertAt = Math.Clamp(insertAt, 0, pendingFrames.Count);
pendingFrames.Insert(insertAt, item);
selectedFrameIndex = insertAt;
OnFramesChanged();
}
private void RestartPreview()
{
previewView.FrameIntervalMs = Math.Max(PREVIEW_MIN_INTERVAL_MS, PREVIEW_BASE_RATE / speedSlider.Value);
previewView.SetFrames(recoloredFrames);
}
private void SelectFrame(int frameIndex)
{
if (frameIndex < 0 || pendingFrames.Count <= frameIndex) return;
selectedFrameIndex = frameIndex;
RebuildFrameGrid();
UpdateFrameActionButtons();
}
private void ValidateFormState()
{
var name = nameTextBox.Text.Trim();
bool hasValidName = !string.IsNullOrWhiteSpace(name);
bool hasValidFrames = pendingFrames.Count >= CustomRunnerRepository.MIN_FRAME_COUNT;
saveButton.Enabled = hasValidName && hasValidFrames;
UpdateFrameActionButtons();
}
private void UpdateFrameActionButtons()
{
var hasSelectedFrame = 0 <= selectedFrameIndex && selectedFrameIndex < pendingFrames.Count;
removeFrameButton.Enabled = hasSelectedFrame;
}
private void UpdateRunnerActionButtons()
{
deleteButton.Enabled = runnerListBox.SelectedIndex >= 0;
}
private void SaveButtonClick(object? sender, EventArgs e)
{
var name = nameTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(name)) return;
if (pendingFrames.Count < CustomRunnerRepository.MIN_FRAME_COUNT) return;
if (repository.Exists(name))
{
var result = MessageBox.Show(
Strings.CustomRunner_ConfirmOverwrite,
Strings.Message_Warning,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result != DialogResult.Yes) return;
}
if (repository.Save(name, pendingFrames))
{
RefreshRunnerList();
for (int i = 0; i < runnerListBox.Items.Count; i++)
{
if (runnerListBox.Items[i] is string itemName &&
itemName.Equals(name, StringComparison.OrdinalIgnoreCase))
{
runnerListBox.SelectedIndex = i;
break;
}
}
ValidateFormState();
}
}
private void DeleteButtonClick(object? sender, EventArgs e)
{
if (runnerListBox.SelectedItem is not string selectedName) return;
var result = MessageBox.Show(
string.Format(Strings.CustomRunner_ConfirmDelete, selectedName),
Strings.Message_Warning,
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
if (result != DialogResult.Yes) return;
repository.Delete(selectedName);
onCustomRunnerDeleted(selectedName);
ClearPendingFrames();
OnFramesChanged();
nameTextBox.Clear();
RefreshRunnerList();
UpdateRunnerActionButtons();
}
private void ClearPendingFrames()
{
previewView.Clear();
foreach (var frame in pendingFrames) frame.Dispose();
pendingFrames.Clear();
foreach (var frame in recoloredFrames) frame.Dispose();
recoloredFrames.Clear();
selectedFrameIndex = -1;
UpdateFrameActionButtons();
}
private static class Palette
{
internal static readonly Color FormBackground = Color.FromArgb(45, 45, 45);
internal static readonly Color ControlBackground = Color.FromArgb(60, 60, 60);
internal static readonly Color ButtonBackground = Color.FromArgb(70, 70, 70);
internal static readonly Color BorderAccent = Color.FromArgb(100, 100, 100);
internal static readonly Color FramePanelBackground = Color.FromArgb(55, 55, 55);
internal static readonly Color FrameThumbnailBackground = Color.FromArgb(40, 40, 40);
internal static readonly Color SelectionHighlight = Color.FromArgb(75, 95, 125);
internal static readonly Color TextPrimary = Color.White;
internal static readonly Color TextSecondary = Color.FromArgb(200, 200, 200);
internal static readonly Color TextMuted = Color.FromArgb(150, 150, 150);
internal static readonly Color TextThumbnailIndex = Color.FromArgb(170, 170, 170);
internal static readonly Color DangerRed = Color.FromArgb(120, 40, 40);
internal static readonly Color DangerBorder = Color.FromArgb(150, 60, 60);
internal static readonly Color AccentBlue = Color.FromArgb(50, 90, 160);
internal static readonly Color AccentBlueBorder = Color.FromArgb(70, 120, 200);
}
private sealed class ThemedButton : Button
{
private static readonly Color DisabledBackColor = Color.FromArgb(58, 58, 58);
private static readonly Color DisabledForeColor = Color.FromArgb(145, 145, 145);
private static readonly Color DisabledBorderColor = Color.FromArgb(82, 82, 82);
protected override void OnEnabledChanged(EventArgs e)
{
base.OnEnabledChanged(e);
Cursor = Enabled ? Cursors.Hand : Cursors.Default;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
var backColor = Enabled ? BackColor : DisabledBackColor;
var foreColor = Enabled ? ForeColor : DisabledForeColor;
var borderColor = Enabled ? FlatAppearance.BorderColor : DisabledBorderColor;
using var backBrush = new SolidBrush(backColor);
using var borderPen = new Pen(borderColor);
e.Graphics.FillRectangle(backBrush, ClientRectangle);
e.Graphics.DrawRectangle(borderPen, 0, 0, Width - 1, Height - 1);
var textSize = TextRenderer.MeasureText(e.Graphics, Text, Font, ClientSize, TextFormatFlags.NoPadding);
var location = new Point(
(Width - textSize.Width) / 2,
(Height - textSize.Height) / 2
);
TextRenderer.DrawText(e.Graphics, Text, Font, location, foreColor, TextFormatFlags.NoPadding);
}
}
}
}