CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
417 lines
13 KiB
C#
417 lines
13 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using FluentValidation;
|
|
|
|
using PCL.Core.App.Localization;
|
|
namespace PCL;
|
|
|
|
public class MyTextBox : TextBox
|
|
{
|
|
public delegate void ValidateChangedEventHandler(object sender, EventArgs e);
|
|
|
|
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius",
|
|
typeof(CornerRadius), typeof(MyTextBox), new PropertyMetadata(new CornerRadius(3d)));
|
|
|
|
public static readonly DependencyProperty ValidateResultProperty = DependencyProperty.Register("ValidateResult",
|
|
typeof(string), typeof(MyTextBox),
|
|
new PropertyMetadata("",
|
|
(d, e) => d.SetValue(IsValidatedPropertyKey,
|
|
string.IsNullOrEmpty((string)e.NewValue))));
|
|
|
|
private static readonly DependencyPropertyKey IsValidatedPropertyKey =
|
|
DependencyProperty.RegisterReadOnly("IsValidated", typeof(bool), typeof(MyTextBox), new PropertyMetadata(true));
|
|
|
|
public static readonly DependencyProperty IsValidatedProperty = IsValidatedPropertyKey.DependencyProperty;
|
|
|
|
public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string),
|
|
typeof(MyTextBox), new PropertyMetadata("", (t, e) =>
|
|
{
|
|
var textBox = (MyTextBox)t;
|
|
textBox.UpdateHintText();
|
|
}));
|
|
|
|
// 额外控件初始化
|
|
|
|
public List<RoutedEventHandler> changedEventList = new();
|
|
|
|
// 提示文本
|
|
|
|
/// <summary>
|
|
/// 是否已经由用户输入过文本,若尚未输入过,则不显示输入检查的失败。
|
|
/// </summary>
|
|
private bool isTextChanged;
|
|
|
|
private ValidateState shownValidateResult = ValidateState.NotInited;
|
|
|
|
// 事件
|
|
|
|
public int Uuid = ModBase.GetUuid();
|
|
|
|
public MyTextBox()
|
|
{
|
|
Loaded += (_, _) => Validate();
|
|
TextChanged += (a, b) => MyTextBox_TextChanged((MyTextBox)a, b);
|
|
IsEnabledChanged += (_, _) => RefreshColor();
|
|
MouseEnter += (_, _) => RefreshColor();
|
|
MouseLeave += (_, _) => RefreshColor();
|
|
GotFocus += (_, _) => RefreshColor();
|
|
LostFocus += (_, _) => RefreshColor();
|
|
IsEnabledChanged += (_, _) => RefreshTextColor();
|
|
}
|
|
|
|
// 自定义属性
|
|
|
|
public bool HasBackground { get; set; } = true;
|
|
public bool ShowValidateResult { get; set; } = true;
|
|
|
|
public CornerRadius CornerRadius
|
|
{
|
|
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
|
set => SetValue(CornerRadiusProperty, value);
|
|
}
|
|
|
|
private TextBlock labWrong
|
|
{
|
|
get
|
|
{
|
|
if (Template is null) return null;
|
|
if (field is null) field = (TextBlock)Template.FindName("labWrong", this);
|
|
return field;
|
|
}
|
|
}
|
|
|
|
private TextBlock labHint
|
|
{
|
|
get
|
|
{
|
|
if (Template is null) return null;
|
|
if (field is null) field = (TextBlock)Template.FindName("labHint", this);
|
|
return field;
|
|
}
|
|
}
|
|
|
|
// 输入验证
|
|
|
|
/// <summary>
|
|
/// 输入验证结果。若为空字符串则无错误,否则为第一个错误原因。
|
|
/// </summary>
|
|
public string ValidateResult
|
|
{
|
|
get => (string)GetValue(ValidateResultProperty);
|
|
set => SetValue(ValidateResultProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否通过了输入验证。
|
|
/// </summary>
|
|
public bool IsValidated => (bool)GetValue(IsValidatedProperty);
|
|
|
|
/// <summary>
|
|
/// 输入验证的规则。
|
|
/// </summary>
|
|
public Collection<IValidator<string>> ValidateRules
|
|
{
|
|
get => field;
|
|
set
|
|
{
|
|
field = value;
|
|
Validate();
|
|
}
|
|
} = new();
|
|
|
|
public string HintText
|
|
{
|
|
get => (string)GetValue(HintTextProperty);
|
|
set => SetValue(HintTextProperty, value);
|
|
}
|
|
|
|
public override void OnApplyTemplate()
|
|
{
|
|
base.OnApplyTemplate();
|
|
if (string.IsNullOrEmpty(HintText) || !string.IsNullOrEmpty(labHint.Text))
|
|
return;
|
|
UpdateHintText();
|
|
}
|
|
|
|
public event ValidateChangedEventHandler? ValidateChanged;
|
|
|
|
public event RoutedEventHandler ValidatedTextChanged
|
|
{
|
|
add => changedEventList.Add(value);
|
|
remove => changedEventList.Remove(value);
|
|
}
|
|
|
|
private void OnValidatedTextChanged(object sender, TextChangedEventArgs e)
|
|
{
|
|
foreach (var handler in changedEventList)
|
|
if (handler is not null)
|
|
handler.Invoke(sender, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 进行输入验证。
|
|
/// </summary>
|
|
public void Validate()
|
|
{
|
|
var stringResult = GetValidateResult();
|
|
|
|
ValidateResult = stringResult;
|
|
// 根据结果改变样式
|
|
if (shownValidateResult != (IsValidated ? ValidateState.Success : ValidateState.FailedAndShowDetail))
|
|
{
|
|
if (IsLoaded && labWrong is not null)
|
|
ChangeValidateResult(IsValidated, true);
|
|
else
|
|
ModBase.RunInNewThread(() =>
|
|
{
|
|
Thread.Sleep(30);
|
|
ModBase.RunInUi(() => ChangeValidateResult(IsValidated, false));
|
|
}, "DelayedValidate Change");
|
|
}
|
|
|
|
// 更新错误信息
|
|
if (ShowValidateResult && !IsValidated)
|
|
{
|
|
if (IsLoaded && labWrong is not null)
|
|
labWrong.Text = ValidateResult;
|
|
else
|
|
ModBase.RunInNewThread(() =>
|
|
{
|
|
var isFinished = false;
|
|
while (!isFinished)
|
|
{
|
|
Thread.Sleep(20);
|
|
ModBase.RunInUiWait(() =>
|
|
{
|
|
if (labWrong is not null)
|
|
{
|
|
labWrong.Text = ValidateResult;
|
|
isFinished = true;
|
|
}
|
|
|
|
if (!IsLoaded)
|
|
isFinished = true;
|
|
});
|
|
}
|
|
}, "DelayedValidate Text");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 强制显示结果为正常,类似尚未输入过文本的状态。不影响实际的检查结果。
|
|
/// </summary>
|
|
public void ForceShowAsSuccess()
|
|
{
|
|
isTextChanged = false;
|
|
ChangeValidateResult(IsValidated, true);
|
|
}
|
|
|
|
private void ChangeValidateResult(bool isSuccessful, bool isLoaded)
|
|
{
|
|
if (isLoaded && ModAnimation.AniControlEnabled == 0 && labWrong is not null)
|
|
{
|
|
if (isSuccessful || !isTextChanged)
|
|
{
|
|
// 变为正确
|
|
shownValidateResult = isSuccessful ? ValidateState.Success : ValidateState.FailedButTextNotChanged;
|
|
ModAnimation.AniStart(
|
|
new[]
|
|
{
|
|
ModAnimation.AaOpacity(labWrong, -labWrong.Opacity, 150),
|
|
ModAnimation.AaHeight(labWrong, -labWrong.Height, 150,
|
|
ease: new ModAnimation.AniEaseOutFluent()),
|
|
ModAnimation.AaCode(() => labWrong.Visibility = Visibility.Collapsed, after: true)
|
|
}, "MyTextBox Validate " + Uuid);
|
|
}
|
|
else if (ShowValidateResult)
|
|
{
|
|
// 变为错误
|
|
shownValidateResult = ValidateState.FailedAndShowDetail;
|
|
labWrong.Visibility = Visibility.Visible;
|
|
ModAnimation.AniStart(
|
|
new[]
|
|
{
|
|
ModAnimation.AaOpacity(labWrong, 1d - labWrong.Opacity, 150),
|
|
ModAnimation.AaHeight(labWrong, 21d - labWrong.Height, 150,
|
|
ease: new ModAnimation.AniEaseOutFluent())
|
|
}, "MyTextBox Validate " + Uuid);
|
|
}
|
|
else
|
|
{
|
|
// 变为错误,但不显示文本
|
|
shownValidateResult = ValidateState.FailedAndHideDetail;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
shownValidateResult = ValidateState.NotLoaded;
|
|
}
|
|
|
|
RefreshColor();
|
|
ValidateChanged?.Invoke(this, new EventArgs());
|
|
}
|
|
|
|
private void MyTextBox_TextChanged(MyTextBox sender, TextChangedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
UpdateHintText();
|
|
// 改变输入记录
|
|
isTextChanged = IsLoaded;
|
|
// 进行输入验证
|
|
Validate();
|
|
if (!IsValidated)
|
|
return;
|
|
// 改变文本
|
|
OnValidatedTextChanged(sender, e);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
"进行输入验证时出错",
|
|
ModBase.LogLevel.Critical,
|
|
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
|
}
|
|
}
|
|
|
|
private void UpdateHintText()
|
|
{
|
|
if (labHint is null)
|
|
return;
|
|
labHint.Text = string.IsNullOrEmpty(Text) ? HintText : "";
|
|
}
|
|
|
|
private string GetValidateResult()
|
|
{
|
|
var stringResult = string.Empty;
|
|
// 执行输入验证
|
|
foreach (var rule in ValidateRules)
|
|
{
|
|
var result = rule.Validate(Text);
|
|
stringResult = result.IsValid ? "" : result.Errors[0].ErrorMessage;
|
|
}
|
|
|
|
return stringResult;
|
|
}
|
|
|
|
// 颜色
|
|
|
|
private void RefreshColor()
|
|
{
|
|
try
|
|
{
|
|
// 不对 ComboBox 从属进行动画
|
|
if (TemplatedParent is not null && TemplatedParent is MyComboBox)
|
|
return;
|
|
// 判断当前颜色
|
|
string foreColorName;
|
|
string backColorName;
|
|
int animationTime;
|
|
if (IsEnabled)
|
|
{
|
|
if (IsValidated || !isTextChanged)
|
|
{
|
|
if (IsFocused)
|
|
{
|
|
foreColorName = "ColorBrush3";
|
|
backColorName = "ColorBrush7";
|
|
animationTime = 10;
|
|
}
|
|
else if (IsMouseOver)
|
|
{
|
|
foreColorName = "ColorBrush4";
|
|
backColorName = "ColorBrush7";
|
|
animationTime = 100;
|
|
}
|
|
else // 未选中
|
|
{
|
|
foreColorName = "ColorBrushBg0";
|
|
backColorName = "ColorBrushHalfWhite";
|
|
animationTime = 100;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreColorName = "ColorBrushRedLight";
|
|
backColorName = "ColorBrushRedBack";
|
|
animationTime = 200;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreColorName = "ColorBrushGray5";
|
|
backColorName = "ColorBrushGray6";
|
|
animationTime = 200;
|
|
}
|
|
|
|
if (!HasBackground)
|
|
backColorName = "ColorBrushTransparent";
|
|
// 触发颜色动画
|
|
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
|
{
|
|
// 有动画
|
|
ModAnimation.AniStart(
|
|
new[]
|
|
{
|
|
ModAnimation.AaColor(this, BorderBrushProperty, foreColorName, animationTime),
|
|
ModAnimation.AaColor(this, BackgroundProperty, backColorName, animationTime)
|
|
}, "MyTextBox Color " + Uuid);
|
|
}
|
|
else
|
|
{
|
|
// 无动画
|
|
ModAnimation.AniStop("MyTextBox Color " + Uuid);
|
|
SetResourceReference(BorderBrushProperty, foreColorName);
|
|
SetResourceReference(BackgroundProperty, backColorName);
|
|
}
|
|
}
|
|
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, "文本框颜色改变出错");
|
|
}
|
|
}
|
|
|
|
private void RefreshTextColor()
|
|
{
|
|
var newColor = IsEnabled ? ThemeManager.colorGray1 : ThemeManager.colorGray4;
|
|
if (((SolidColorBrush)Foreground).Color.R == newColor.r)
|
|
return;
|
|
if (IsLoaded && ModAnimation.AniControlEnabled == 0 && !string.IsNullOrEmpty(Text))
|
|
{
|
|
// 有动画
|
|
ModAnimation.AniStart(
|
|
new[]
|
|
{
|
|
ModAnimation.AaColor(this, ForegroundProperty, IsEnabled ? "ColorBrushGray1" : "ColorBrushGray4",
|
|
200)
|
|
}, "MyTextBox TextColor " + Uuid);
|
|
}
|
|
else
|
|
{
|
|
// 无动画
|
|
ModAnimation.AniStop("MyTextBox TextColor " + Uuid);
|
|
Foreground = newColor;
|
|
}
|
|
}
|
|
|
|
// 在按下回车时触发自定义事件
|
|
private void MyTextBox_KeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Enter) ModMain.RaiseCustomEvent(this);
|
|
}
|
|
private enum ValidateState
|
|
{
|
|
NotInited,
|
|
Success,
|
|
FailedButTextNotChanged,
|
|
FailedAndShowDetail,
|
|
FailedAndHideDetail,
|
|
NotLoaded
|
|
}
|
|
}
|