mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-19 18:47:55 +08:00
feat: 增強自动战斗文件选择功能,支持多级路径和相对目录 (#15174)
* chore: 先依照保全派駐週期粗略分類
* chore: 換個位置
* chore: 換成用地圖名稱分類
* chore: 再改 再改
* chore: 別空白了 伺服器會被殺死的
* chore: 補上下拉選單顯示的 json 檔案名稱,避免解析錯誤
* chore: 幫展開後的下拉選單加上寬度限制
* Revert "chore: 幫展開後的下拉選單加上寬度限制"
This reverts commit d5f80c9d6e.
* feat: 新增文件树视图项样式
* perf: 优化自动战斗下拉列表
* feat: 统一样式
* chore: 删除多余调用
---------
Co-authored-by: uye <99072975+ABA2396@users.noreply.github.com>
This commit is contained in:
34
src/MaaWpfGui/Models/CopilotFileItem.cs
Normal file
34
src/MaaWpfGui/Models/CopilotFileItem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// <copyright file="CopilotFileItem.cs" company="MaaAssistantArknights">
|
||||
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
|
||||
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License v3.0 only as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY
|
||||
// </copyright>
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace MaaWpfGui.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 文件项数据模型,用于 TreeView 显示
|
||||
/// </summary>
|
||||
public class CopilotFileItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string? FullPath { get; set; }
|
||||
|
||||
public string? RelativePath { get; set; }
|
||||
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
public ObservableCollection<CopilotFileItem> Children { get; set; } = [];
|
||||
}
|
||||
@@ -14,5 +14,6 @@
|
||||
<ResourceDictionary Source="Styles/DataGrid.xaml" />
|
||||
<ResourceDictionary Source="Styles/PasswordBox.xaml" />
|
||||
<ResourceDictionary Source="Styles/ToolTip.xaml" />
|
||||
<ResourceDictionary Source="Styles/TreeView.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
57
src/MaaWpfGui/Res/Styles/TreeView.xaml
Normal file
57
src/MaaWpfGui/Res/Styles/TreeView.xaml
Normal file
@@ -0,0 +1,57 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<!-- 文件树视图项样式 -->
|
||||
<Style
|
||||
x:Key="FileTreeViewItemStyle"
|
||||
BasedOn="{StaticResource TreeViewItemBaseStyle}"
|
||||
TargetType="{x:Type TreeViewItem}">
|
||||
<Setter Property="IsExpanded" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TreeViewItem}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition MinHeight="{TemplateBinding MinHeight}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
Name="Bd"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"
|
||||
SnapsToDevicePixels="true">
|
||||
<DockPanel Margin="{Binding Converter={StaticResource TreeViewItemMarginConverter}, RelativeSource={RelativeSource TemplatedParent}}" LastChildFill="True">
|
||||
<ToggleButton
|
||||
Name="Expander"
|
||||
ClickMode="Press"
|
||||
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Style="{StaticResource ExpandCollapseToggleStyle}" />
|
||||
<ContentPresenter
|
||||
Name="PART_Header"
|
||||
VerticalAlignment="Center"
|
||||
ContentSource="Header"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ItemsPresenter Name="ItemsHost" Grid.Row="1" />
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger SourceName="Bd" Property="UIElement.IsMouseOver" Value="true">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource MouseOverRegionBrushOpacity75}" />
|
||||
</Trigger>
|
||||
<Trigger Property="TreeViewItem.IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource MouseOverRegionBrushOpacity75}" />
|
||||
</Trigger>
|
||||
<Trigger Property="TreeViewItem.IsExpanded" Value="False">
|
||||
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
|
||||
</Trigger>
|
||||
<Trigger Property="ItemsControl.HasItems" Value="False">
|
||||
<Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -59,6 +59,7 @@ public partial class CopilotViewModel : Screen
|
||||
private readonly List<int> _copilotIdList = []; // 用于保存作业列表中的作业的Id,对于同一个作业,只有都执行成功才点赞
|
||||
private readonly List<int> _recentlyRatedCopilotId = []; // TODO: 可能考虑加个持久化
|
||||
private AsstTaskType _taskType = AsstTaskType.Copilot;
|
||||
private readonly Dictionary<string, string> _copilotJsonPathMap = new(); // 下拉框与实际作业 json 档案路径对照表
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的已解析作业,非即时添加的作业会使用该缓存
|
||||
@@ -79,6 +80,22 @@ public partial class CopilotViewModel : Screen
|
||||
/// </summary>
|
||||
public ObservableCollection<LogItemViewModel> LogItemViewModels { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file items for TreeView.
|
||||
/// </summary>
|
||||
public ObservableCollection<CopilotFileItem> FileItems { get; } = [];
|
||||
|
||||
private bool _isFilePopupOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets whether the file dropdown popup is open.
|
||||
/// </summary>
|
||||
public bool IsFilePopupOpen
|
||||
{
|
||||
get => _isFilePopupOpen;
|
||||
set => SetAndNotify(ref _isFilePopupOpen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or private sets the view models of Copilot items.
|
||||
/// </summary>
|
||||
@@ -239,6 +256,33 @@ public partial class CopilotViewModel : Screen
|
||||
}
|
||||
}
|
||||
|
||||
private string _displayFilename = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display filename (relative path).
|
||||
/// </summary>
|
||||
public string DisplayFilename
|
||||
{
|
||||
get => _displayFilename;
|
||||
set
|
||||
{
|
||||
SetAndNotify(ref _displayFilename, value);
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var copilotRoot = Path.Combine(ResourceDir, "copilot");
|
||||
var fullPath = Path.IsPathRooted(value) ? value : Path.Combine(copilotRoot, value);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
Filename = fullPath;
|
||||
}
|
||||
else if (_copilotJsonPathMap.TryGetValue(Path.GetFileName(value), out var mappedPath))
|
||||
{
|
||||
Filename = mappedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _filename = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
@@ -250,14 +294,45 @@ public partial class CopilotViewModel : Screen
|
||||
set {
|
||||
if (!File.Exists(value))
|
||||
{
|
||||
var resourceFile = Path.Combine(ResourceDir, "copilot", Path.GetFileName(value));
|
||||
if (File.Exists(resourceFile))
|
||||
// 从对照表取得完整 json 档案路径
|
||||
if (_copilotJsonPathMap.TryGetValue(value, out var fullPath))
|
||||
{
|
||||
value = resourceFile;
|
||||
value = fullPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
var resourceFile = Path.Combine(ResourceDir, "copilot", Path.GetFileName(value));
|
||||
if (File.Exists(resourceFile))
|
||||
{
|
||||
value = resourceFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetAndNotify(ref _filename, value);
|
||||
|
||||
// 更新显示文件名(相对路径)
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var copilotRoot = Path.Combine(ResourceDir, "copilot");
|
||||
if (value.StartsWith(copilotRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(copilotRoot, value);
|
||||
_displayFilename = relativePath;
|
||||
NotifyOfPropertyChange(nameof(DisplayFilename));
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayFilename = value;
|
||||
NotifyOfPropertyChange(nameof(DisplayFilename));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayFilename = string.Empty;
|
||||
NotifyOfPropertyChange(nameof(DisplayFilename));
|
||||
}
|
||||
|
||||
ClearLog();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
@@ -1301,28 +1376,150 @@ public partial class CopilotViewModel : Screen
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On comboBox drop down opened.
|
||||
/// Load file items for TreeView.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
[UsedImplicitly]
|
||||
public void OnDropDownOpened(object sender, EventArgs e)
|
||||
public void LoadFileItems()
|
||||
{
|
||||
if (sender is not ComboBox comboBox)
|
||||
try
|
||||
{
|
||||
_copilotJsonPathMap.Clear();
|
||||
FileItems.Clear();
|
||||
|
||||
var copilotRoot = Path.Combine(ResourceDir, "copilot");
|
||||
|
||||
// 获取根目录下的所有目录和文件
|
||||
var directories = Directory.GetDirectories(copilotRoot);
|
||||
var rootFiles = Directory.GetFiles(copilotRoot, "*.json");
|
||||
|
||||
// 添加根目录下的文件
|
||||
foreach (var file in rootFiles)
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
var relativePath = Path.GetRelativePath(copilotRoot, file);
|
||||
_copilotJsonPathMap[fileName] = file;
|
||||
|
||||
FileItems.Add(new CopilotFileItem
|
||||
{
|
||||
Name = fileName,
|
||||
FullPath = file,
|
||||
RelativePath = relativePath,
|
||||
IsFolder = false,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加根目录下的文件夹(支持嵌套,old 文件夹放在最后)
|
||||
var oldFolderItem = (CopilotFileItem?)null;
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var dirName = Path.GetFileName(dir);
|
||||
var folderItem = LoadFolderItem(dir, copilotRoot);
|
||||
if (folderItem != null)
|
||||
{
|
||||
if (dirName.Equals("old", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
oldFolderItem = folderItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileItems.Add(folderItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 old 文件夹添加到最后
|
||||
if (oldFolderItem != null)
|
||||
{
|
||||
FileItems.Add(oldFolderItem);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
FileItems.Clear();
|
||||
AddLog(exception.Message, UiLogColor.Error, showTime: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归加载文件夹项(支持嵌套子文件夹)
|
||||
/// </summary>
|
||||
/// <param name="dirPath">文件夹路径</param>
|
||||
/// <param name="copilotRoot">copilot 根目录路径</param>
|
||||
/// <returns>文件项,如果文件夹为空则返回 null</returns>
|
||||
private CopilotFileItem? LoadFolderItem(string dirPath, string copilotRoot)
|
||||
{
|
||||
var dirName = Path.GetFileName(dirPath);
|
||||
var folderItem = new CopilotFileItem
|
||||
{
|
||||
Name = dirName,
|
||||
IsFolder = true,
|
||||
};
|
||||
|
||||
// 获取文件夹下的所有文件
|
||||
var folderFiles = Directory.GetFiles(dirPath, "*.json");
|
||||
foreach (var file in folderFiles)
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
var relativePath = Path.GetRelativePath(copilotRoot, file);
|
||||
_copilotJsonPathMap[fileName] = file;
|
||||
|
||||
folderItem.Children.Add(new CopilotFileItem
|
||||
{
|
||||
Name = fileName,
|
||||
FullPath = file,
|
||||
RelativePath = relativePath,
|
||||
IsFolder = false,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文件夹下的所有子文件夹(递归加载)
|
||||
var subDirectories = Directory.GetDirectories(dirPath);
|
||||
foreach (var subDir in subDirectories)
|
||||
{
|
||||
var subFolderItem = LoadFolderItem(subDir, copilotRoot);
|
||||
if (subFolderItem != null)
|
||||
{
|
||||
folderItem.Children.Add(subFolderItem);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果文件夹为空(既没有文件也没有子文件夹),返回 null
|
||||
if (folderItem.Children.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return folderItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle file selection from TreeView.
|
||||
/// </summary>
|
||||
/// <param name="fileItem">The selected file item.</param>
|
||||
[UsedImplicitly]
|
||||
public void OnFileSelected(CopilotFileItem? fileItem)
|
||||
{
|
||||
if (fileItem == null || fileItem.IsFolder || string.IsNullOrEmpty(fileItem.FullPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
Filename = fileItem.FullPath;
|
||||
IsFilePopupOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle file popup.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public void ToggleFilePopup()
|
||||
{
|
||||
if (!IsFilePopupOpen)
|
||||
{
|
||||
var files = Directory.GetFiles(Path.Combine(ResourceDir, "copilot"), "*.json");
|
||||
comboBox.ItemsSource = files.Select(Path.GetFileName).ToList();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
comboBox.ItemsSource = null;
|
||||
AddLog(exception.Message, UiLogColor.Error, showTime: false);
|
||||
LoadFileItems();
|
||||
}
|
||||
|
||||
IsFilePopupOpen = !IsFilePopupOpen;
|
||||
}
|
||||
|
||||
private async Task AddCopilotTaskToList(string? stageName, bool isRaid)
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<TabControl.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource TabItemSliding}" TargetType="{x:Type TabItem}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource RegionBrush}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
@@ -83,15 +83,81 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ComboBox
|
||||
Grid.Column="0"
|
||||
Height="30"
|
||||
hc:BorderElement.CornerRadius="4,0,0,4"
|
||||
hc:InfoElement.Placeholder="{DynamicResource CopilotLocation}"
|
||||
DropDownOpened="{s:Action OnDropDownOpened}"
|
||||
IsEditable="True"
|
||||
IsReadOnly="{c:Binding Path=!Idle}"
|
||||
Text="{Binding Filename}" />
|
||||
<Grid Grid.Column="0">
|
||||
<TextBox
|
||||
x:Name="FileTextBox"
|
||||
Height="30"
|
||||
hc:BorderElement.CornerRadius="4,0,0,4"
|
||||
hc:InfoElement.Placeholder="{DynamicResource CopilotLocation}"
|
||||
IsEnabled="{Binding Idle}"
|
||||
Text="{Binding DisplayFilename, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Height="28"
|
||||
Margin="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{DynamicResource RegionBrushOpacity50}"
|
||||
BorderThickness="0"
|
||||
Click="{s:Action ToggleFilePopup}"
|
||||
Cursor="Hand"
|
||||
IsEnabled="{Binding Idle}">
|
||||
<StackPanel>
|
||||
<Path
|
||||
Width="12"
|
||||
Height="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Data="{StaticResource DownGeometry}"
|
||||
Fill="{DynamicResource PrimaryTextBrush}"
|
||||
Stretch="Uniform"
|
||||
Visibility="{c:Binding !IsFilePopupOpen}" />
|
||||
<Path
|
||||
Width="12"
|
||||
Height="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
d:Visibility="Collapsed"
|
||||
Data="{StaticResource UpGeometry}"
|
||||
Fill="{DynamicResource PrimaryTextBrush}"
|
||||
Stretch="Uniform"
|
||||
Visibility="{c:Binding IsFilePopupOpen}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Popup
|
||||
MinWidth="{c:Binding 'ActualWidth + 90',
|
||||
ElementName=FileTextBox}"
|
||||
IsOpen="{Binding IsFilePopupOpen}"
|
||||
Placement="Bottom"
|
||||
PlacementTarget="{Binding ElementName=FileTextBox}"
|
||||
StaysOpen="True">
|
||||
<Border
|
||||
MinWidth="{c:Binding 'ActualWidth + 90',
|
||||
ElementName=FileTextBox}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<TreeView
|
||||
x:Name="FileTreeView"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{Binding FileItems}"
|
||||
SelectedItemChanged="FileTreeView_SelectedItemChanged">
|
||||
<TreeView.ItemTemplate>
|
||||
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||
<controls:TextBlock
|
||||
Margin="5"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding Name}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource FileTreeViewItemStyle}" TargetType="{x:Type TreeViewItem}" />
|
||||
</TreeView.ItemContainerStyle>
|
||||
</TreeView>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Height="30"
|
||||
|
||||
37
src/MaaWpfGui/Views/UI/CopilotView.xaml.cs
Normal file
37
src/MaaWpfGui/Views/UI/CopilotView.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// <copyright file="CopilotView.xaml.cs" company="MaaAssistantArknights">
|
||||
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
|
||||
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License v3.0 only as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY
|
||||
// </copyright>
|
||||
|
||||
using System.Windows;
|
||||
using MaaWpfGui.Models;
|
||||
using MaaWpfGui.ViewModels.UI;
|
||||
|
||||
namespace MaaWpfGui.Views.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for CopilotView.xaml
|
||||
/// </summary>
|
||||
public partial class CopilotView
|
||||
{
|
||||
public CopilotView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void FileTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||
{
|
||||
if (DataContext is CopilotViewModel viewModel && e.NewValue is CopilotFileItem fileItem && !fileItem.IsFolder)
|
||||
{
|
||||
viewModel.OnFileSelected(fileItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user