Files
2026-07-13 12:26:32 +08:00

901 lines
34 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.
// 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);
}
}
}
}