Files
DCIT/DCIT.App/Forms/MainForm.cs
2026-07-13 10:04:36 +08:00

3380 lines
144 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using DCIT.App.Presentation;
using DCIT.Core.Models;
using DCIT.Core.Services;
using DCIT.Core.Utilities;
namespace DCIT.App.Forms
{
public sealed class MainForm : Form
{
private readonly ConfigurationService _configurationService;
private readonly LogService _logService;
private readonly FileScanService _fileScanService;
private readonly RuleService _ruleService;
private readonly AutoExtractService _autoExtractService;
private readonly IDocumentProcessingService _documentProcessingService;
private readonly IUserDialogService _dialogService;
private readonly BatchExecutionPlanner _batchExecutionPlanner = new BatchExecutionPlanner();
private readonly BindingList<FileScanItem> _replaceItems = new BindingList<FileScanItem>();
private readonly BindingList<FileScanItem> _restoreItems = new BindingList<FileScanItem>();
private readonly BindingList<RuleDefinition> _rules = new BindingList<RuleDefinition>();
private readonly TabControl _tabControl = new TabControl();
private readonly TabPage _replaceTab = new TabPage("替换");
private readonly TabPage _restoreTab = new TabPage("恢复");
private readonly TabPage _settingsTab = new TabPage("设置");
private readonly TextBox _txtReplaceSource = new TextBox();
private readonly TextBox _txtReplaceOutput = new TextBox();
private readonly TextBox _txtRuleFile = new TextBox();
private readonly TextBox _txtRestoreSource = new TextBox();
private readonly TextBox _txtRestoreOutput = new TextBox();
private readonly ComboBox _cmbRestoreDiffInput = new ComboBox();
private readonly DataGridView _gridReplace = new DataGridView();
private readonly DataGridView _gridRestore = new DataGridView();
private readonly DataGridView _gridRules = new DataGridView();
private SplitContainer _settingsRuleListSplitContainer;
private readonly RichTextBox _logBox = new RichTextBox();
private readonly ContextMenuStrip _logContextMenu = new ContextMenuStrip();
private readonly ContextMenuStrip _replaceGridContextMenu = new ContextMenuStrip();
private readonly ContextMenuStrip _restoreGridContextMenu = new ContextMenuStrip();
private readonly ContextMenuStrip _ruleGridContextMenu = new ContextMenuStrip();
private readonly Button _btnAutoExtract = new Button();
private readonly Button _btnStartReplace = new Button();
private readonly Button _btnStopReplace = new Button();
private readonly Button _btnStartRestore = new Button();
private readonly Button _btnStopRestore = new Button();
private readonly Button _btnBrowseReplaceSource = new Button();
private readonly Button _btnBrowseReplaceOutput = new Button();
private readonly Button _btnSelectAllReplace = new Button();
private readonly Button _btnClearReplace = new Button();
private readonly Button _btnBrowseRestoreSource = new Button();
private readonly Button _btnBrowseRestoreOutput = new Button();
private readonly Button _btnSelectAllRestore = new Button();
private readonly Button _btnClearRestore = new Button();
private readonly Button _btnUndoAutoExtract = new Button();
private readonly Button _btnSaveRules = new Button();
private readonly Button _btnOpenRules = new Button();
private readonly ProgressBar _replaceTotalProgress = new ProgressBar();
private readonly ProgressBar _replaceFileProgress = new ProgressBar();
private readonly ProgressBar _restoreTotalProgress = new ProgressBar();
private readonly ProgressBar _restoreFileProgress = new ProgressBar();
private readonly Label _lblReplaceRuntime = new Label();
private readonly Label _lblRestoreRuntime = new Label();
private readonly CheckBox _chkJsonDiffEncryption = new CheckBox();
private readonly CheckBox _chkEnableImageReplacement = new CheckBox();
private readonly CheckBox _chkUseFixedImageSeed = new CheckBox();
private readonly CheckBox _chkExtractKeywords = new CheckBox();
private readonly CheckBox _chkExtractWords = new CheckBox();
private readonly CheckBox _chkExtractSentences = new CheckBox();
private readonly CheckBox _chkKeywordFrequency = new CheckBox();
private readonly CheckBox _chkKeywordTfIdf = new CheckBox();
private readonly CheckBox _chkKeywordTextRank = new CheckBox();
private readonly CheckBox _chkChineseWords = new CheckBox();
private readonly CheckBox _chkEnglishWords = new CheckBox();
private readonly CheckBox _chkChineseSentences = new CheckBox();
private readonly CheckBox _chkEnglishSentences = new CheckBox();
private readonly CheckBox _chkExcludeEmails = new CheckBox();
private readonly NumericUpDown _numKeywordCount = new NumericUpDown();
private readonly NumericUpDown _numWordCount = new NumericUpDown();
private readonly NumericUpDown _numSentenceCount = new NumericUpDown();
private readonly NumericUpDown _numFixedImageSeed = new NumericUpDown();
private readonly NumericUpDown _numMinChineseLength = new NumericUpDown();
private readonly NumericUpDown _numMinEnglishLength = new NumericUpDown();
private readonly TextBox _txtSentenceDelimiters = new TextBox();
private AppConfig _config;
private ConfigurationLoadResult _configurationLoadResult;
private RuleFile _ruleFile = new RuleFile();
private List<string> _lastAutoExtractRuleIds = new List<string>();
private CancellationTokenSource _replaceScanCancellation;
private CancellationTokenSource _restoreScanCancellation;
private CancellationTokenSource _autoExtractCancellation;
private CancellationTokenSource _replaceExecutionCancellation;
private CancellationTokenSource _restoreExecutionCancellation;
private bool _isAutoExtractRunning;
private bool _isReplaceRunning;
private bool _isRestoreRunning;
private bool _skipConfigSaveOnClose;
private bool _settingsRuleListInitialWidthApplied;
private int _lastReplaceRuntimeFlushTick;
private int _lastRestoreRuntimeFlushTick;
private const int RuntimeUiRefreshIntervalMilliseconds = 100;
public MainForm(
ConfigurationService configurationService,
LogService logService,
FileScanService fileScanService,
RuleService ruleService,
AutoExtractService autoExtractService,
IDocumentProcessingService documentProcessingService,
IUserDialogService dialogService)
{
_configurationService = configurationService;
_logService = logService;
_fileScanService = fileScanService;
_ruleService = ruleService;
_autoExtractService = autoExtractService;
_documentProcessingService = documentProcessingService;
_dialogService = dialogService;
_configurationLoadResult = _configurationService.Load();
_config = _configurationLoadResult.Config;
Text = "WORD 2007 格式调整软件";
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(1400, 900);
Width = 1600;
Height = 980;
Font = new Font("Microsoft YaHei UI", 9F);
InitializeLayout();
BindData();
ApplyConfigToUi();
_logService.EntryWritten += LogService_EntryWritten;
_logService.WriteFailed += LogService_WriteFailed;
FormClosing += MainForm_FormClosing;
Shown += MainForm_Shown;
}
private void InitializeLayout()
{
var splitContainer = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
SplitterDistance = 720,
};
_tabControl.Dock = DockStyle.Fill;
_tabControl.TabPages.Add(_replaceTab);
_tabControl.TabPages.Add(_restoreTab);
_tabControl.TabPages.Add(_settingsTab);
splitContainer.Panel1.Controls.Add(_tabControl);
_logBox.Dock = DockStyle.Fill;
_logBox.ReadOnly = true;
_logBox.BackColor = Color.White;
_logBox.Font = new Font("Consolas", 10F);
splitContainer.Panel2.Controls.Add(_logBox);
Controls.Add(splitContainer);
BuildReplaceTab();
BuildRestoreTabV2();
BuildSettingsTab();
ConfigureContextMenus();
}
private void BuildReplaceTab()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var pathPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 6,
AutoSize = true,
Padding = new Padding(8),
};
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
_btnBrowseReplaceSource.Text = "选择原始文件夹";
_btnBrowseReplaceSource.Dock = DockStyle.Fill;
_btnBrowseReplaceOutput.Text = "选择替换文件夹";
_btnBrowseReplaceOutput.Dock = DockStyle.Fill;
_btnSelectAllReplace.Text = "全选";
_btnSelectAllReplace.AutoSize = true;
_btnClearReplace.Text = "全不选";
_btnClearReplace.AutoSize = true;
_btnAutoExtract.Text = "自动提取";
_btnStartReplace.Text = "开始替换";
_btnStopReplace.Text = "停止";
_btnStopReplace.Enabled = false;
_lblReplaceRuntime.AutoSize = true;
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
AddLabeledRow(pathPanel, 0, "原始文件夹", _txtReplaceSource, _btnBrowseReplaceSource, "替换文件夹", _txtReplaceOutput, _btnBrowseReplaceOutput);
_btnBrowseReplaceSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceSource, RefreshReplaceScanAsync); };
_btnBrowseReplaceOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceOutput, RefreshReplaceScanAsync); };
_txtReplaceSource.Leave += async delegate { await RefreshReplaceScanAsync(); };
_txtReplaceOutput.Leave += async delegate { await RefreshReplaceScanAsync(); };
_btnSelectAllReplace.Click += delegate { SetSelection(_replaceItems, true); };
_btnClearReplace.Click += delegate { SetSelection(_replaceItems, false); };
_btnAutoExtract.Click += async delegate { await RunAutoExtractAsync(); };
_btnStartReplace.Click += StartReplaceExecution_Click;
_btnStopReplace.Click += StopReplaceOrAutoExtract_Click;
ConfigureFileGrid(_gridReplace, isRestoreGrid: false);
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
_gridReplace.Dock = DockStyle.Fill;
gridPanel.Controls.Add(_gridReplace);
var bottomPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 1,
AutoSize = true,
Padding = new Padding(8),
};
var buttonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
};
buttonPanel.Controls.Add(_btnSelectAllReplace);
buttonPanel.Controls.Add(_btnClearReplace);
buttonPanel.Controls.Add(_btnAutoExtract);
buttonPanel.Controls.Add(_btnStartReplace);
buttonPanel.Controls.Add(_btnStopReplace);
bottomPanel.Controls.Add(buttonPanel);
bottomPanel.Controls.Add(CreateProgressLine("总进度", _replaceTotalProgress));
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _replaceFileProgress));
bottomPanel.Controls.Add(_lblReplaceRuntime);
root.Controls.Add(pathPanel, 0, 0);
root.Controls.Add(gridPanel, 0, 1);
root.Controls.Add(bottomPanel, 0, 2);
_replaceTab.Controls.Add(root);
}
private void BuildRestoreTab()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var pathPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 6,
AutoSize = true,
Padding = new Padding(8),
};
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
var btnBrowseRestoreSource = new Button { Text = "选择替换文件夹", Dock = DockStyle.Fill };
var btnBrowseRestoreOutput = new Button { Text = "选择恢复文件夹", Dock = DockStyle.Fill };
var btnSelectAllRestore = new Button { Text = "全选", AutoSize = true };
var btnClearRestore = new Button { Text = "全不选", AutoSize = true };
_btnStartRestore.Text = "开始恢复";
_btnStopRestore.Text = "停止";
_btnStopRestore.Enabled = false;
_lblRestoreRuntime.AutoSize = true;
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, btnBrowseRestoreOutput);
btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); };
btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); };
_txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); };
_txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); };
btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); };
btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); };
_btnStartRestore.Click += StartRestoreExecution_Click;
ConfigureFileGrid(_gridRestore, isRestoreGrid: true);
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
_gridRestore.Dock = DockStyle.Fill;
gridPanel.Controls.Add(_gridRestore);
var bottomPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 1,
AutoSize = true,
Padding = new Padding(8),
};
var buttonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
};
buttonPanel.Controls.Add(btnSelectAllRestore);
buttonPanel.Controls.Add(btnClearRestore);
buttonPanel.Controls.Add(_btnStartRestore);
buttonPanel.Controls.Add(_btnStopRestore);
bottomPanel.Controls.Add(buttonPanel);
bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress));
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress));
bottomPanel.Controls.Add(_lblRestoreRuntime);
root.Controls.Add(pathPanel, 0, 0);
root.Controls.Add(gridPanel, 0, 1);
root.Controls.Add(bottomPanel, 0, 2);
_restoreTab.Controls.Add(root);
}
private void BuildRestoreTabV2()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var pathPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 6,
AutoSize = true,
Padding = new Padding(8),
};
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
_btnBrowseRestoreSource.Text = "选择替换文件夹";
_btnBrowseRestoreSource.Dock = DockStyle.Fill;
_btnBrowseRestoreOutput.Text = "选择恢复文件夹";
_btnBrowseRestoreOutput.Dock = DockStyle.Fill;
_btnSelectAllRestore.Text = "全选";
_btnSelectAllRestore.AutoSize = true;
_btnClearRestore.Text = "全不选";
_btnClearRestore.AutoSize = true;
_btnStartRestore.Text = "开始恢复";
_btnStopRestore.Text = "停止";
_btnStopRestore.Enabled = false;
_lblRestoreRuntime.AutoSize = true;
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
_cmbRestoreDiffInput.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbRestoreDiffInput.Items.Clear();
_cmbRestoreDiffInput.Items.Add(".diff");
_cmbRestoreDiffInput.Items.Add(".bmp");
_cmbRestoreDiffInput.SelectedIndex = 0;
AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, _btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, _btnBrowseRestoreOutput);
pathPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
pathPanel.Controls.Add(new Label { Text = "差异文件格式", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 1);
pathPanel.Controls.Add(_cmbRestoreDiffInput, 1, 1);
_btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); };
_btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); };
_txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); };
_txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); };
_cmbRestoreDiffInput.SelectedIndexChanged += async delegate { await RefreshRestoreScanAsync(); };
_btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); };
_btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); };
_btnStartRestore.Click += StartRestoreExecution_Click;
_btnStopRestore.Click += StopRestore_Click;
ConfigureFileGrid(_gridRestore, isRestoreGrid: true);
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
_gridRestore.Dock = DockStyle.Fill;
gridPanel.Controls.Add(_gridRestore);
var bottomPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 1,
AutoSize = true,
Padding = new Padding(8),
};
var buttonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
};
buttonPanel.Controls.Add(_btnSelectAllRestore);
buttonPanel.Controls.Add(_btnClearRestore);
buttonPanel.Controls.Add(_btnStartRestore);
buttonPanel.Controls.Add(_btnStopRestore);
bottomPanel.Controls.Add(buttonPanel);
bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress));
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress));
bottomPanel.Controls.Add(_lblRestoreRuntime);
root.Controls.Add(pathPanel, 0, 0);
root.Controls.Add(gridPanel, 0, 1);
root.Controls.Add(bottomPanel, 0, 2);
_restoreTab.Controls.Add(root);
}
private void BuildSettingsTab()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
var topPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 5,
AutoSize = true,
Padding = new Padding(8),
};
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140F));
_btnOpenRules.Text = "打开规则";
_btnSaveRules.Text = "保存规则";
_btnUndoAutoExtract.Text = "撤销自动提取";
_btnUndoAutoExtract.Enabled = false;
topPanel.Controls.Add(new Label { Text = "规则文件", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0);
_txtRuleFile.Dock = DockStyle.Fill;
topPanel.Controls.Add(_txtRuleFile, 1, 0);
topPanel.Controls.Add(_btnOpenRules, 2, 0);
topPanel.Controls.Add(_btnSaveRules, 3, 0);
topPanel.Controls.Add(_btnUndoAutoExtract, 4, 0);
_btnOpenRules.Click += OpenRules_Click;
_btnSaveRules.Click += SaveRules_Click;
_btnUndoAutoExtract.Click += UndoAutoExtract_Click;
var actionPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
Padding = new Padding(8, 0, 8, 8),
};
var btnAddRule = new Button { Text = "新增规则", AutoSize = true };
var btnDeleteRule = new Button { Text = "删除规则", AutoSize = true };
var btnMoveUp = new Button { Text = "上移", AutoSize = true };
var btnMoveDown = new Button { Text = "下移", AutoSize = true };
btnAddRule.Click += AddRule_Click;
btnDeleteRule.Click += DeleteRule_Click;
btnMoveUp.Click += MoveRuleUp_Click;
btnMoveDown.Click += MoveRuleDown_Click;
actionPanel.Controls.Add(btnAddRule);
actionPanel.Controls.Add(btnDeleteRule);
actionPanel.Controls.Add(btnMoveUp);
actionPanel.Controls.Add(btnMoveDown);
var settingsAndGrid = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical,
SplitterDistance = 350,
};
_settingsRuleListSplitContainer = settingsAndGrid;
settingsAndGrid.HandleCreated += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
settingsAndGrid.SizeChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
settingsAndGrid.VisibleChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
_tabControl.SelectedIndexChanged += async delegate
{
ApplySettingsRuleListInitialWidth(settingsAndGrid);
await RefreshScanOnTabSwitchAsync();
};
settingsAndGrid.Panel1.Controls.Add(BuildAutoExtractSettingsGroup());
settingsAndGrid.Panel2.Controls.Add(BuildRulesGridPanel());
root.Controls.Add(topPanel, 0, 0);
root.Controls.Add(actionPanel, 0, 1);
root.Controls.Add(settingsAndGrid, 0, 2);
_settingsTab.Controls.Add(root);
}
private void ApplySettingsRuleListInitialWidth(SplitContainer splitContainer)
{
if (_settingsRuleListInitialWidthApplied || splitContainer == null || splitContainer.Orientation != Orientation.Vertical)
{
return;
}
if (splitContainer.ClientSize.Width < 600)
{
return;
}
var availableWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth;
if (availableWidth <= 0 || availableWidth < splitContainer.Panel1MinSize + splitContainer.Panel2MinSize)
{
return;
}
var leftWidth = availableWidth / 3;
var maxLeftWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth - splitContainer.Panel2MinSize;
if (maxLeftWidth < splitContainer.Panel1MinSize)
{
return;
}
leftWidth = Math.Max(splitContainer.Panel1MinSize, Math.Min(maxLeftWidth, leftWidth));
splitContainer.SplitterDistance = leftWidth;
_settingsRuleListInitialWidthApplied = true;
}
private void ConfigureContextMenus()
{
ConfigureLogContextMenu();
ConfigureFileGridContextMenu(_gridReplace, _replaceGridContextMenu);
ConfigureFileGridContextMenu(_gridRestore, _restoreGridContextMenu);
ConfigureRuleGridContextMenu();
}
private void ConfigureLogContextMenu()
{
_logContextMenu.Items.Clear();
_logContextMenu.Items.Add("复制全部内容", null, delegate { CopyAllLogs(); });
_logContextMenu.Items.Add("清空日志", null, delegate { ClearLogBox(); });
_logBox.ContextMenuStrip = _logContextMenu;
}
private void ConfigureFileGridContextMenu(DataGridView grid, ContextMenuStrip menu)
{
menu.Items.Clear();
menu.Items.Add("删除", null, delegate
{
if (ReferenceEquals(grid, _gridReplace))
{
DeleteSelectedFileItems(_replaceItems, grid);
}
else
{
DeleteSelectedFileItems(_restoreItems, grid);
}
});
menu.Items.Add("启用", null, delegate { SetSelectedFileItems(grid, true); });
menu.Items.Add("不启用", null, delegate { SetSelectedFileItems(grid, false); });
grid.ContextMenuStrip = menu;
}
private void ConfigureRuleGridContextMenu()
{
_ruleGridContextMenu.Items.Clear();
_ruleGridContextMenu.Items.Add("删除", null, delegate { DeleteSelectedRules(); });
_ruleGridContextMenu.Items.Add("启用", null, delegate { SetSelectedRulesEnabled(true); });
_ruleGridContextMenu.Items.Add("不启用", null, delegate { SetSelectedRulesEnabled(false); });
_gridRules.ContextMenuStrip = _ruleGridContextMenu;
}
private Control BuildAutoExtractSettingsGroup()
{
var group = new GroupBox
{
Text = "自动提取设置",
Dock = DockStyle.Fill,
Padding = new Padding(12),
};
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
AutoScroll = true,
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55F));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45F));
_chkJsonDiffEncryption.Text = "JSON 差异文件加密";
_chkEnableImageReplacement.Text = "启用图片替换";
_chkUseFixedImageSeed.Text = "使用固定图像随机种子";
_chkExtractKeywords.Text = "提取关键字";
_chkExtractWords.Text = "提取高频词";
_chkExtractSentences.Text = "提取高频句";
_chkKeywordFrequency.Text = "关键字算法: 词频";
_chkKeywordTfIdf.Text = "关键字算法: TF-IDF";
_chkKeywordTextRank.Text = "关键字算法: TextRank";
_chkChineseWords.Text = "统计中文词";
_chkEnglishWords.Text = "统计英文词";
_chkChineseSentences.Text = "统计中文句";
_chkEnglishSentences.Text = "统计英文句";
_chkExcludeEmails.Text = "排除邮箱地址";
ConfigureNumeric(_numKeywordCount, 1, 1000);
ConfigureNumeric(_numWordCount, 1, 1000);
ConfigureNumeric(_numSentenceCount, 1, 1000);
ConfigureNumeric(_numFixedImageSeed, 0, int.MaxValue);
ConfigureNumeric(_numMinChineseLength, 1, 50);
ConfigureNumeric(_numMinEnglishLength, 1, 50);
AddSettingRow(layout, 0, _chkJsonDiffEncryption, new Label());
AddSettingRow(layout, 1, _chkEnableImageReplacement, new Label());
AddSettingRow(layout, 2, _chkUseFixedImageSeed, _numFixedImageSeed);
AddSettingRow(layout, 3, _chkExtractKeywords, _chkKeywordFrequency);
AddSettingRow(layout, 4, _chkExtractWords, _chkKeywordTfIdf);
AddSettingRow(layout, 5, _chkExtractSentences, _chkKeywordTextRank);
AddSettingRow(layout, 6, new Label { Text = "关键字最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numKeywordCount);
AddSettingRow(layout, 7, new Label { Text = "高频词最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numWordCount);
AddSettingRow(layout, 8, new Label { Text = "高频句最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numSentenceCount);
AddSettingRow(layout, 9, _chkChineseWords, _chkEnglishWords);
AddSettingRow(layout, 10, _chkChineseSentences, _chkEnglishSentences);
AddSettingRow(layout, 11, new Label { Text = "中文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinChineseLength);
AddSettingRow(layout, 12, new Label { Text = "英文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinEnglishLength);
AddSettingRow(layout, 13, new Label { Text = "句子分隔符", Anchor = AnchorStyles.Left, AutoSize = true }, _txtSentenceDelimiters);
AddSettingRow(layout, 14, _chkExcludeEmails, new Label());
_chkUseFixedImageSeed.CheckedChanged += delegate
{
_numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked;
};
group.Controls.Add(layout);
return group;
}
private Control BuildRulesGridPanel()
{
ConfigureRuleGrid(_gridRules);
var panel = new Panel
{
Dock = DockStyle.Fill,
Padding = new Padding(8),
};
panel.Controls.Add(_gridRules);
_gridRules.Dock = DockStyle.Fill;
return panel;
}
private void BindData()
{
_gridReplace.DataSource = _replaceItems;
_gridRestore.DataSource = _restoreItems;
_gridRules.DataSource = _rules;
}
private void ApplyConfigToUi()
{
_txtReplaceSource.Text = _config.ReplacePage.SourceDirectory ?? string.Empty;
_txtReplaceOutput.Text = _config.ReplacePage.OutputDirectory ?? string.Empty;
_txtRuleFile.Text = _config.ReplacePage.RuleFilePath ?? string.Empty;
_txtRestoreSource.Text = _config.RestorePage.ReplaceDirectory ?? string.Empty;
_txtRestoreOutput.Text = _config.RestorePage.RestoreDirectory ?? string.Empty;
_cmbRestoreDiffInput.SelectedItem = string.Equals(_config.RestorePage.DiffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase)
? ".bmp"
: ".diff";
var auto = _config.Settings.AutoExtract;
_chkJsonDiffEncryption.Checked = _config.Settings.JsonDiffEncryption;
_chkEnableImageReplacement.Checked = _config.Settings.EnableImageReplacement;
_chkUseFixedImageSeed.Checked = _config.Settings.UseFixedImageSeed;
_numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, _config.Settings.FixedImageSeed));
_numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked;
_chkExtractKeywords.Checked = auto.ExtractKeywords;
_chkExtractWords.Checked = auto.ExtractHighFrequencyWords;
_chkExtractSentences.Checked = auto.ExtractHighFrequencySentences;
_chkKeywordFrequency.Checked = auto.UseKeywordFrequency;
_chkKeywordTfIdf.Checked = auto.UseKeywordTfIdf;
_chkKeywordTextRank.Checked = auto.UseKeywordTextRank;
_chkChineseWords.Checked = auto.CountChineseWords;
_chkEnglishWords.Checked = auto.CountEnglishWords;
_chkChineseSentences.Checked = auto.CountChineseSentences;
_chkEnglishSentences.Checked = auto.CountEnglishSentences;
_chkExcludeEmails.Checked = auto.ExcludeEmailAddresses;
_numKeywordCount.Value = auto.MaxKeywordCount;
_numWordCount.Value = auto.MaxWordCount;
_numSentenceCount.Value = auto.MaxSentenceCount;
_numMinChineseLength.Value = auto.MinChineseWordLength;
_numMinEnglishLength.Value = auto.MinEnglishWordLength;
_txtSentenceDelimiters.Text = auto.SentenceDelimiters ?? string.Empty;
if (!string.IsNullOrWhiteSpace(_txtRuleFile.Text) && File.Exists(_txtRuleFile.Text))
{
LoadRuleFile(_txtRuleFile.Text);
}
}
private void CaptureUiToConfig()
{
_config.ReplacePage.SourceDirectory = _txtReplaceSource.Text.Trim();
_config.ReplacePage.OutputDirectory = _txtReplaceOutput.Text.Trim();
_config.ReplacePage.RuleFilePath = _txtRuleFile.Text.Trim();
_config.RestorePage.ReplaceDirectory = _txtRestoreSource.Text.Trim();
_config.RestorePage.RestoreDirectory = _txtRestoreOutput.Text.Trim();
_config.RestorePage.DiffInputFormat = GetSelectedRestoreDiffInputFormat();
var auto = _config.Settings.AutoExtract;
_config.Settings.JsonDiffEncryption = _chkJsonDiffEncryption.Checked;
_config.Settings.EnableImageReplacement = _chkEnableImageReplacement.Checked;
_config.Settings.UseFixedImageSeed = _chkUseFixedImageSeed.Checked;
_config.Settings.FixedImageSeed = Decimal.ToInt32(_numFixedImageSeed.Value);
auto.ExtractKeywords = _chkExtractKeywords.Checked;
auto.ExtractHighFrequencyWords = _chkExtractWords.Checked;
auto.ExtractHighFrequencySentences = _chkExtractSentences.Checked;
auto.UseKeywordFrequency = _chkKeywordFrequency.Checked;
auto.UseKeywordTfIdf = _chkKeywordTfIdf.Checked;
auto.UseKeywordTextRank = _chkKeywordTextRank.Checked;
auto.CountChineseWords = _chkChineseWords.Checked;
auto.CountEnglishWords = _chkEnglishWords.Checked;
auto.CountChineseSentences = _chkChineseSentences.Checked;
auto.CountEnglishSentences = _chkEnglishSentences.Checked;
auto.ExcludeEmailAddresses = _chkExcludeEmails.Checked;
auto.MaxKeywordCount = Decimal.ToInt32(_numKeywordCount.Value);
auto.MaxWordCount = Decimal.ToInt32(_numWordCount.Value);
auto.MaxSentenceCount = Decimal.ToInt32(_numSentenceCount.Value);
auto.MinChineseWordLength = Decimal.ToInt32(_numMinChineseLength.Value);
auto.MinEnglishWordLength = Decimal.ToInt32(_numMinEnglishLength.Value);
auto.SentenceDelimiters = _txtSentenceDelimiters.Text;
}
private async void MainForm_Shown(object sender, EventArgs e)
{
_logService.Info("APP_START", "程序启动。");
if (_configurationLoadResult.IsCorrupted)
{
var message = "config.json 已损坏或无法解析,当前禁止执行替换、恢复和自动提取。是否重建默认配置?"
+ Environment.NewLine
+ _configurationLoadResult.ErrorMessage;
var result = ShowMessage(message, "配置文件损坏", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
{
_config = _configurationService.CreateDefault();
ApplyConfigToUi();
_configurationService.Save(_config);
_logService.Warning("CONFIG_RESET", "配置文件已重建为默认值。", _configurationLoadResult.ErrorMessage);
}
else
{
_logService.Error("CONFIG_BLOCK", "配置文件损坏且用户拒绝重建默认配置。", _configurationLoadResult.ErrorMessage);
_skipConfigSaveOnClose = true;
Close();
return;
}
}
else if (_configurationLoadResult.IsMissing)
{
_logService.Info("CONFIG_DEFAULT", "config.json 不存在,当前使用默认配置。");
}
await RefreshReplaceScanAsync();
await RefreshRestoreScanAsync();
UpdateActionStates();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (_isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning)
{
e.Cancel = true;
ShowMessage("当前仍有任务在执行,请先停止任务并等待当前文件处理完成。", "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
if (!_skipConfigSaveOnClose)
{
CaptureUiToConfig();
_configurationService.Save(_config);
}
}
catch (Exception ex)
{
ShowMessage("保存配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void LogService_EntryWritten(object sender, LogEntry entry)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object, LogEntry>(LogService_EntryWritten), sender, entry);
return;
}
var color = Color.Black;
switch (entry.Level)
{
case LogLevel.Info:
color = Color.Green;
break;
case LogLevel.Warning:
color = Color.DarkOrange;
break;
case LogLevel.Error:
color = Color.Red;
break;
}
_logBox.SelectionStart = _logBox.TextLength;
_logBox.SelectionColor = color;
_logBox.AppendText(string.Format("{0:HH:mm:ss} [{1}] {2} {3}", entry.Timestamp, entry.Level.ToString().ToUpperInvariant(), entry.EventCode, entry.Message) + Environment.NewLine);
if (!string.IsNullOrWhiteSpace(entry.Detail))
{
_logBox.SelectionColor = Color.Black;
_logBox.AppendText(IndentBlock(entry.Detail) + Environment.NewLine);
}
_logBox.SelectionStart = _logBox.TextLength;
_logBox.ScrollToCaret();
FlushLogBox();
}
private void LogService_WriteFailed(object sender, LogWriteFailureEventArgs e)
{
if (InvokeRequired)
{
BeginInvoke(new Action<object, LogWriteFailureEventArgs>(LogService_WriteFailed), sender, e);
return;
}
var detail = string.Format(
"日志文件写入失败,当前日志路径: {0}{1}{2}",
e.LogFilePath,
Environment.NewLine,
e.Exception.Message);
_logBox.SelectionStart = _logBox.TextLength;
_logBox.SelectionColor = Color.Red;
_logBox.AppendText(string.Format("{0:HH:mm:ss} [ERROR] LOG_WRITE_FAIL 日志文件写入失败", DateTime.Now) + Environment.NewLine);
_logBox.SelectionColor = Color.Black;
_logBox.AppendText(IndentBlock(detail) + Environment.NewLine);
_logBox.SelectionStart = _logBox.TextLength;
_logBox.ScrollToCaret();
FlushLogBox();
if (_isAutoExtractRunning && _autoExtractCancellation != null && !_autoExtractCancellation.IsCancellationRequested)
{
_autoExtractCancellation.Cancel();
}
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
{
_replaceExecutionCancellation.Cancel();
}
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
{
_restoreExecutionCancellation.Cancel();
}
ShowMessage(
"日志文件当前无法继续写入。当前正在处理的文件会在可控状态结束后停止,后续文件不会继续处理。"
+ Environment.NewLine
+ detail,
"日志异常",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
private void FlushLogBox()
{
FlushControl(_logBox, forceRefresh: false);
}
private string GetAllLogText()
{
return _logBox.Text;
}
private static string IndentBlock(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
return " " + normalized.Replace("\n", Environment.NewLine + " ");
}
private string BuildReplaceScanStartDetail(string sourceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "原始文件夹", sourceDirectory);
AppendKeyValue(builder, "替换文件夹(输入)", configuredOutputDirectory);
AppendKeyValue(builder, "替换文件夹(生效)", effectiveOutputDirectory);
return builder.ToString().TrimEnd();
}
private string BuildReplaceScanDoneDetail(IList<FileScanItem> items)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString());
AppendScanItems(builder, items);
return builder.ToString().TrimEnd();
}
private string BuildRestoreScanStartDetail(string replaceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory, string diffInputFormat)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "替换文件夹", replaceDirectory);
AppendKeyValue(builder, "恢复文件夹(输入)", configuredOutputDirectory);
AppendKeyValue(builder, "恢复文件夹(生效)", effectiveOutputDirectory);
AppendKeyValue(builder, "差异文件格式", diffInputFormat);
return builder.ToString().TrimEnd();
}
private string BuildRestoreScanDoneDetail(IList<FileScanItem> items, string diffInputFormat)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString());
AppendKeyValue(builder, "差异文件格式", diffInputFormat);
AppendScanItems(builder, items);
return builder.ToString().TrimEnd();
}
private string BuildReplaceBatchPlanDetail(IList<FileScanItem> selectedItems, BatchExecutionPlan plan, RuleFile executionRuleFile, bool encryptDiff, int? fixedImageSeed)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "原始文件夹", _txtReplaceSource.Text.Trim());
AppendKeyValue(builder, "替换文件夹", _txtReplaceOutput.Text.Trim());
AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim());
AppendKeyValue(builder, "差异 JSON 加密", encryptDiff ? "是" : "否");
AppendKeyValue(builder, "图片替换", _chkEnableImageReplacement.Checked ? "启用" : "禁用");
AppendKeyValue(builder, "固定图片种子", fixedImageSeed.HasValue ? fixedImageSeed.Value.ToString() : "否");
AppendKeyValue(builder, "规则总数", executionRuleFile == null || executionRuleFile.Rules == null ? "0" : executionRuleFile.Rules.Count.ToString());
AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString());
AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString());
AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString());
AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString());
if (plan != null && plan.SkippedItems.Count > 0)
{
builder.AppendLine("预跳过项目:");
foreach (var skipped in plan.SkippedItems)
{
builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath));
AppendKeyValue(builder, " 原因", skipped.Message);
AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath);
}
}
builder.AppendLine("执行项目:");
AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList());
return builder.ToString().TrimEnd();
}
private string BuildRestoreBatchPlanDetail(IList<FileScanItem> selectedItems, BatchExecutionPlan plan)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "替换文件夹", _txtRestoreSource.Text.Trim());
AppendKeyValue(builder, "恢复文件夹", _txtRestoreOutput.Text.Trim());
AppendKeyValue(builder, "差异文件格式", GetSelectedRestoreDiffInputFormat());
AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim());
AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString());
AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString());
AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString());
AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString());
if (plan != null && plan.SkippedItems.Count > 0)
{
builder.AppendLine("预跳过项目:");
foreach (var skipped in plan.SkippedItems)
{
builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath));
AppendKeyValue(builder, " 原因", skipped.Message);
AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath);
}
}
builder.AppendLine("执行项目:");
AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList());
return builder.ToString().TrimEnd();
}
private string BuildReplaceFileStartDetail(FileScanItem item, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null);
}
private string BuildReplaceFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail(
"替换",
item,
displayIndex,
totalCount,
includeResultFiles: true,
textOperationCount: result == null ? null : (int?)result.TextOperationCount,
imageOperationCount: result == null ? null : (int?)result.ImageOperationCount,
bmpPath: result == null ? null : result.BmpPath,
exception: null);
}
private string BuildReplaceFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception);
}
private string BuildRestoreFileStartDetail(FileScanItem item, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null);
}
private string BuildRestoreFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail(
"恢复",
item,
displayIndex,
totalCount,
includeResultFiles: true,
textOperationCount: result == null ? null : (int?)result.TextOperationCount,
imageOperationCount: result == null ? null : (int?)result.ImageOperationCount,
bmpPath: result == null ? null : result.BmpPath,
exception: null);
}
private string BuildRestoreFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount)
{
return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception);
}
private string BuildFileExecutionDetail(
string operationName,
FileScanItem item,
int displayIndex,
int totalCount,
bool includeResultFiles,
int? textOperationCount,
int? imageOperationCount,
string bmpPath,
Exception exception)
{
var builder = new StringBuilder();
AppendKeyValue(builder, "操作", operationName);
AppendKeyValue(builder, "序号", string.Format("{0}/{1}", displayIndex, totalCount));
AppendFileSnapshot(builder, "源文件", item == null ? null : item.SourcePath);
AppendKeyValue(builder, "目标输出", item == null ? null : item.OutputPath);
AppendKeyValue(builder, "差异文件", item == null ? null : item.DiffPath);
if (includeResultFiles)
{
AppendFileSnapshot(builder, "目标输出", item == null ? null : item.OutputPath);
AppendFileSnapshot(builder, "差异文件", item == null ? null : item.DiffPath);
AppendFileSnapshot(builder, "BMP 差异文件", bmpPath);
}
if (textOperationCount.HasValue)
{
AppendKeyValue(builder, "文本命中/恢复数", textOperationCount.Value.ToString());
}
if (imageOperationCount.HasValue)
{
AppendKeyValue(builder, "图片命中/恢复数", imageOperationCount.Value.ToString());
}
AppendExceptionDetail(builder, exception);
return builder.ToString().TrimEnd();
}
private static void AppendScanItems(StringBuilder builder, IList<FileScanItem> items)
{
if (builder == null)
{
return;
}
if (items == null || items.Count == 0)
{
builder.AppendLine("候选项: <empty>");
return;
}
var maxCount = Math.Min(items.Count, 20);
for (var index = 0; index < maxCount; index++)
{
var item = items[index];
builder.AppendLine(string.Format("[{0}]", index + 1));
AppendFileSnapshot(builder, " 源文件", item == null ? null : item.SourcePath);
AppendKeyValue(builder, " 目标输出", item == null ? null : item.OutputPath);
AppendKeyValue(builder, " 差异文件", item == null ? null : item.DiffPath);
AppendKeyValue(builder, " 勾选", item != null && item.IsSelected ? "是" : "否");
AppendKeyValue(builder, " 状态", item == null ? null : item.Status.ToString());
}
if (items.Count > maxCount)
{
builder.AppendLine(string.Format("... 其余 {0} 项省略", items.Count - maxCount));
}
}
private static void AppendFileSnapshot(StringBuilder builder, string label, string path)
{
if (builder == null)
{
return;
}
AppendKeyValue(builder, label + "路径", path);
if (string.IsNullOrWhiteSpace(path))
{
return;
}
try
{
if (!File.Exists(path))
{
AppendKeyValue(builder, label + "存在", "否");
return;
}
var info = new FileInfo(path);
AppendKeyValue(builder, label + "存在", "是");
AppendKeyValue(builder, label + "大小(bytes)", info.Length.ToString());
AppendKeyValue(builder, label + "属性", info.Attributes.ToString());
AppendKeyValue(builder, label + "只读", info.IsReadOnly ? "是" : "否");
AppendKeyValue(builder, label + "最后写入时间", info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss.fff"));
AppendKeyValue(builder, label + "最后写入时间(UTC)", info.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff"));
AppendKeyValue(builder, label + "SHA256", SafeComputeFileFingerprint(path));
}
catch (Exception ex)
{
AppendKeyValue(builder, label + "快照错误", ex.GetType().Name + ": " + ex.Message);
}
}
private static void AppendExceptionDetail(StringBuilder builder, Exception exception)
{
if (builder == null || exception == null)
{
return;
}
AppendKeyValue(builder, "异常类型", exception.GetType().FullName);
AppendKeyValue(builder, "异常消息", exception.Message);
if (exception.InnerException != null)
{
AppendKeyValue(builder, "内部异常", exception.InnerException.GetType().FullName + ": " + exception.InnerException.Message);
}
AppendKeyValue(builder, "异常堆栈", exception.ToString());
}
private static void AppendKeyValue(StringBuilder builder, string key, string value)
{
if (builder == null)
{
return;
}
builder.Append(key ?? string.Empty);
builder.Append(": ");
builder.AppendLine(NormalizeDetailValue(value));
}
private static string SafeComputeFileFingerprint(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return "<empty>";
}
try
{
return HashUtility.ComputeFileSha256Base64(path);
}
catch (Exception ex)
{
return "<unavailable: " + ex.GetType().Name + ": " + ex.Message + ">";
}
}
private static string NormalizeDetailValue(string value)
{
return string.IsNullOrWhiteSpace(value) ? "<empty>" : value;
}
private void CopyAllLogs()
{
var text = GetAllLogText();
if (string.IsNullOrEmpty(text))
{
return;
}
try
{
Clipboard.SetText(text);
}
catch
{
}
}
private void ClearLogBox()
{
_logBox.Clear();
}
private async Task RefreshScanOnTabSwitchAsync()
{
try
{
if (_tabControl.SelectedTab == _replaceTab)
{
await RefreshReplaceScanAsync();
}
else if (_tabControl.SelectedTab == _restoreTab)
{
await RefreshRestoreScanAsync();
}
}
catch (OperationCanceledException)
{
}
}
private async Task RefreshReplaceScanAsync()
{
_replaceScanCancellation?.Cancel();
_replaceScanCancellation = new CancellationTokenSource();
var token = _replaceScanCancellation.Token;
try
{
var source = _txtReplaceSource.Text.Trim();
if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source))
{
_replaceItems.Clear();
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
FlushReplaceRuntimeUi(forceRefresh: true);
UpdateActionStates();
return;
}
var output = GetReplacePreviewOutputDirectory(source);
if (!string.IsNullOrWhiteSpace(_txtReplaceOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output))
{
foreach (var message in _fileScanService.ValidateReplaceDirectories(source, _txtReplaceOutput.Text.Trim()))
{
_logService.Error("SCAN_BLOCK", message);
}
}
_logService.Info("SCAN_START", "开始扫描替换候选文件。", BuildReplaceScanStartDetail(source, _txtReplaceOutput.Text.Trim(), output));
var ruleFile = CreateExecutionRuleFile();
var items = await Task.Run(() => _fileScanService.ScanReplaceCandidates(source, output, ruleFile), token);
if (token.IsCancellationRequested)
{
return;
}
ReplaceBinding(_replaceItems, items);
_lblReplaceRuntime.Text = string.Format(
"当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0",
items.Count);
FlushReplaceRuntimeUi(forceRefresh: true);
_logService.Info("SCAN_DONE", string.Format("替换候选文件扫描完成,共 {0} 项。", items.Count), BuildReplaceScanDoneDetail(items));
UpdateActionStates();
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logService.Error("SCAN_FAIL", "替换候选文件扫描失败。", ex.Message);
}
}
private async Task RefreshRestoreScanAsync()
{
_restoreScanCancellation?.Cancel();
_restoreScanCancellation = new CancellationTokenSource();
var token = _restoreScanCancellation.Token;
try
{
var source = _txtRestoreSource.Text.Trim();
if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source))
{
_restoreItems.Clear();
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
FlushRestoreRuntimeUi(forceRefresh: true);
UpdateActionStates();
return;
}
var output = GetRestorePreviewOutputDirectory(source);
var diffInputFormat = GetSelectedRestoreDiffInputFormat();
if (!string.IsNullOrWhiteSpace(_txtRestoreOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output))
{
foreach (var message in _fileScanService.ValidateRestoreDirectories(source, _txtRestoreOutput.Text.Trim()))
{
_logService.Error("RESTORE_SCAN_BLOCK", message);
}
}
_logService.Info("RESTORE_SCAN_START", "开始扫描恢复候选文件。", BuildRestoreScanStartDetail(source, _txtRestoreOutput.Text.Trim(), output, diffInputFormat));
var items = await Task.Run(() => _fileScanService.ScanRestoreCandidates(source, output, diffInputFormat), token);
if (token.IsCancellationRequested)
{
return;
}
ReplaceBinding(_restoreItems, items);
_lblRestoreRuntime.Text = string.Format(
"当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0",
items.Count);
FlushRestoreRuntimeUi(forceRefresh: true);
_logService.Info("RESTORE_SCAN_DONE", string.Format("恢复候选文件扫描完成,共 {0} 项。", items.Count), BuildRestoreScanDoneDetail(items, diffInputFormat));
UpdateActionStates();
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logService.Error("RESTORE_SCAN_FAIL", "恢复候选文件扫描失败。", ex.Message);
}
}
private async Task RunAutoExtractAsync()
{
var selectedFiles = _replaceItems.Where(item => item.IsSelected).Select(item => item.SourcePath).ToList();
if (selectedFiles.Count == 0)
{
ShowMessage("未勾选任何文件,无法执行自动提取。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
CaptureUiToConfig();
_autoExtractCancellation = new CancellationTokenSource();
_isAutoExtractRunning = true;
SetRuleEditingEnabled(false);
_btnStopReplace.Enabled = true;
UpdateActionStates();
_replaceTotalProgress.Maximum = Math.Max(selectedFiles.Count, 1);
_replaceTotalProgress.Value = 0;
_replaceFileProgress.Value = 0;
_logService.Info("EXTRACT_START", string.Format("开始自动提取,勾选文件数={0}。", selectedFiles.Count));
try
{
var progress = new Progress<ExtractionProgress>(value =>
{
_replaceTotalProgress.Maximum = Math.Max(value.Total, 1);
_replaceTotalProgress.Value = Math.Min(value.Current, _replaceTotalProgress.Maximum);
_replaceFileProgress.Style = ProgressBarStyle.Marquee;
_lblReplaceRuntime.Text = value.Message;
});
var result = await _autoExtractService.ExtractAsync(
selectedFiles,
_config.Settings.AutoExtract,
_rules.ToList(),
progress,
_autoExtractCancellation.Token);
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
_replaceFileProgress.Value = 100;
foreach (var warning in result.Warnings)
{
_logService.Warning("EXTRACT_SKIP", warning);
}
if (result.Candidates.Count == 0)
{
ShowMessage("未提取到可用项。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
_logService.Info("EXTRACT_DONE", "自动提取完成,但未提取到可用项。");
return;
}
var preview = _dialogService.ShowAutoExtractPreview(this, result.Candidates);
if (preview.DialogResult != DialogResult.OK)
{
_logService.Info("EXTRACT_DONE", "自动提取预览已取消,未追加规则。");
return;
}
var selectedCandidates = preview.SelectedCandidates ?? Array.Empty<ExtractionCandidate>();
AppendAutoExtractRules(selectedCandidates);
var skippedCount = result.Candidates.Count - selectedCandidates.Count;
var summary = string.Format("自动提取完成。提取={0},追加={1},跳过={2}。", result.Candidates.Count, selectedCandidates.Count, skippedCount);
ShowMessage(summary, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
_logService.Info("EXTRACT_DONE", summary);
}
catch (OperationCanceledException)
{
_logService.Warning("EXTRACT_CANCEL", "自动提取已停止。");
}
catch (Exception ex)
{
_logService.Error("EXTRACT_FAIL", "自动提取失败。", ex.ToString());
ShowMessage("自动提取失败: " + ex.Message, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isAutoExtractRunning = false;
SetRuleEditingEnabled(true);
_btnStopReplace.Enabled = false;
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
UpdateActionStates();
}
}
private void AppendAutoExtractRules(IList<ExtractionCandidate> candidates)
{
if (candidates.Count == 0)
{
return;
}
var newRuleIds = new List<string>();
foreach (var candidate in candidates)
{
var rule = _ruleService.CreateAutoExtractRule(candidate, _rules.ToList());
_rules.Add(rule);
newRuleIds.Add(rule.Id);
}
_lastAutoExtractRuleIds = newRuleIds;
_btnUndoAutoExtract.Enabled = true;
var report = _ruleService.Validate(new RuleFile { Rules = _rules.ToList() });
if (report.Issues.Count > 0)
{
var text = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
ShowMessage(text, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
foreach (var issue in report.Issues)
{
LogRuleIssue(issue);
}
}
_tabControl.SelectedTab = _settingsTab;
if (_rules.Count > 0)
{
var firstIndex = _rules.ToList().FindIndex(rule => newRuleIds.Contains(rule.Id));
if (firstIndex >= 0)
{
_gridRules.ClearSelection();
if (_gridRules.Rows[firstIndex].Cells.Count > 0)
{
_gridRules.CurrentCell = _gridRules.Rows[firstIndex].Cells[0];
}
_gridRules.Rows[firstIndex].Selected = true;
try
{
_gridRules.FirstDisplayedScrollingRowIndex = firstIndex;
}
catch (InvalidOperationException)
{
}
}
}
}
private void OpenRules_Click(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
dialog.Filter = "规则文件 (*.rule)|*.rule|所有文件 (*.*)|*.*";
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
LoadRuleFile(dialog.FileName);
_txtRuleFile.Text = dialog.FileName;
}
}
private void SaveRules_Click(object sender, EventArgs e)
{
try
{
var path = _txtRuleFile.Text.Trim();
if (string.IsNullOrWhiteSpace(path))
{
using (var dialog = new SaveFileDialog())
{
dialog.Filter = "规则文件 (*.rule)|*.rule";
dialog.DefaultExt = "rule";
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
path = dialog.FileName;
_txtRuleFile.Text = path;
}
}
_ruleFile.Rules = _rules.ToList();
var report = _ruleService.Validate(_ruleFile);
foreach (var issue in report.Issues)
{
LogRuleIssue(issue);
}
if (report.Issues.Count > 0)
{
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
ShowMessage(summary, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
if (report.HasErrors)
{
return;
}
}
_ruleService.Save(path, _ruleFile);
_logService.Info("RULE_SAVE", "规则文件已保存。", path);
}
catch (Exception ex)
{
_logService.Error("RULE_SAVE_FAIL", "保存规则文件失败。", ex.Message);
ShowMessage("保存规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void UndoAutoExtract_Click(object sender, EventArgs e)
{
if (_lastAutoExtractRuleIds.Count == 0)
{
return;
}
for (var index = _rules.Count - 1; index >= 0; index--)
{
if (_lastAutoExtractRuleIds.Contains(_rules[index].Id))
{
_rules.RemoveAt(index);
}
}
_logService.Info("EXTRACT_UNDO", string.Format("已撤销最近一次自动提取追加的 {0} 条规则。", _lastAutoExtractRuleIds.Count));
_lastAutoExtractRuleIds.Clear();
_btnUndoAutoExtract.Enabled = false;
}
private void AddRule_Click(object sender, EventArgs e)
{
var nextIndex = _rules.Count + 1;
_rules.Add(new RuleDefinition
{
Id = "MR" + nextIndex.ToString("0000"),
Name = "MANUAL-" + nextIndex.ToString("0000"),
IsEnabled = true,
MatchMode = RuleMatchMode.PlainText,
Target = RuleTarget.DocumentContent,
Note = string.Empty,
});
}
private void DeleteRule_Click(object sender, EventArgs e)
{
DeleteSelectedRules();
}
private void MoveRuleUp_Click(object sender, EventArgs e)
{
MoveCurrentRule(-1);
}
private void MoveRuleDown_Click(object sender, EventArgs e)
{
MoveCurrentRule(1);
}
private void MoveCurrentRule(int delta)
{
if (_gridRules.CurrentRow == null)
{
return;
}
var currentIndex = _gridRules.CurrentRow.Index;
var nextIndex = currentIndex + delta;
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= _rules.Count)
{
return;
}
var item = _rules[currentIndex];
_rules.RemoveAt(currentIndex);
_rules.Insert(nextIndex, item);
_gridRules.ClearSelection();
_gridRules.Rows[nextIndex].Selected = true;
}
private async void StartReplaceExecution_Click(object sender, EventArgs e)
{
if (_isReplaceRunning || _isRestoreRunning)
{
return;
}
if (!TryPersistConfigFromUi())
{
return;
}
var selectedItems = _replaceItems.Where(item => item.IsSelected).ToList();
if (selectedItems.Count == 0)
{
ShowMessage("未勾选任何替换文件。", "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var validationMessages = GetReplaceExecutionValidationMessages();
if (validationMessages.Count > 0)
{
foreach (var message in validationMessages)
{
_logService.Error("REPLACE_BLOCK", message);
}
ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
RuleFile executionRuleFile;
if (!PrepareRuleExecutionWithBlockingErrors(out executionRuleFile))
{
return;
}
var emptyReplacementRules = _rules.Where(rule => rule.IsEnabled && string.IsNullOrEmpty(rule.ReplacementText)).ToList();
if (emptyReplacementRules.Count > 0)
{
var warning = string.Format("当前存在 {0} 条启用规则的替换文本为空,执行后会删除命中的原始内容。是否继续?", emptyReplacementRules.Count);
if (ShowMessage(warning, "强提示", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
{
return;
}
}
await ExecuteReplaceBatchAsync(selectedItems, executionRuleFile);
}
private async void StartRestoreExecution_Click(object sender, EventArgs e)
{
if (_isReplaceRunning || _isRestoreRunning)
{
return;
}
if (!TryPersistConfigFromUi())
{
return;
}
var selectedItems = _restoreItems.Where(item => item.IsSelected).ToList();
if (selectedItems.Count == 0)
{
ShowMessage("未勾选任何恢复文件。", "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var validationMessages = GetRestoreExecutionValidationMessages();
if (validationMessages.Count > 0)
{
foreach (var message in validationMessages)
{
_logService.Error("RESTORE_BLOCK", message);
}
ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
await ExecuteRestoreBatchAsync(selectedItems);
}
private void StopReplaceOrAutoExtract_Click(object sender, EventArgs e)
{
if (_isAutoExtractRunning)
{
_autoExtractCancellation?.Cancel();
return;
}
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
{
_replaceExecutionCancellation.Cancel();
_logService.Warning("REPLACE_STOP", "已请求停止替换任务,当前文件完成后停止。");
}
}
private void StopRestore_Click(object sender, EventArgs e)
{
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
{
_restoreExecutionCancellation.Cancel();
_logService.Warning("RESTORE_STOP", "已请求停止恢复任务,当前文件完成后停止。");
}
}
private async Task ExecuteReplaceBatchAsync(IList<FileScanItem> selectedItems, RuleFile executionRuleFile)
{
_replaceExecutionCancellation?.Dispose();
_replaceExecutionCancellation = new CancellationTokenSource();
_isReplaceRunning = true;
SetRuleEditingEnabled(false);
var encryptDiff = _chkJsonDiffEncryption.Checked;
var fixedImageSeed = _chkUseFixedImageSeed.Checked ? Decimal.ToInt32(_numFixedImageSeed.Value) : (int?)null;
foreach (var item in selectedItems)
{
item.Status = ProcessingStatus.Pending;
item.TextOperationCount = 0;
item.ImageOperationCount = 0;
}
ResetReplaceProgress(selectedItems.Count);
UpdateActionStates();
var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount);
var executionItems = plan.ExecutionItems.ToList();
var displayOrder = executionItems
.Select((item, index) => new { item, index = index + 1 })
.ToDictionary(entry => entry.item, entry => entry.index);
var successCount = 0;
var failedCount = 0;
var stoppedCount = 0;
var totalHits = 0;
var completedCount = 0;
var nextExecutionIndex = -1;
foreach (var skipped in plan.SkippedItems)
{
skipped.Item.Status = ProcessingStatus.Failed;
RefreshReplaceItemRow(skipped.Item, forceRefresh: true);
failedCount++;
completedCount++;
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
FlushReplaceRuntimeUi(forceRefresh: true);
_logService.Warning(
"REPLACE_BATCH_SKIP",
skipped.Message,
string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath));
}
try
{
_logService.Info(
"REPLACE_BATCH_PLAN",
string.Format(
"替换批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。",
selectedItems.Count,
executionItems.Count,
plan.SkippedItems.Count,
plan.DegreeOfParallelism,
plan.RequiresSerialExecution ? "(包含 .doc已退回串行" : string.Empty),
BuildReplaceBatchPlanDetail(selectedItems, plan, executionRuleFile, encryptDiff, fixedImageSeed));
async Task RunWorkerAsync()
{
while (true)
{
if (_replaceExecutionCancellation.IsCancellationRequested)
{
return;
}
var executionIndex = Interlocked.Increment(ref nextExecutionIndex);
if (executionIndex >= executionItems.Count)
{
return;
}
var item = executionItems[executionIndex];
var displayIndex = displayOrder[item];
item.Status = ProcessingStatus.InProgress;
RefreshReplaceItemRow(item, forceRefresh: true);
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
_logService.Info("DEBUG_RULES", "执行规则列表。",
string.Format("总规则数={0}; 规则详情=[{1}]",
executionRuleFile.Rules.Count,
string.Join("; ", executionRuleFile.Rules.Select(r =>
string.Format("Id={0}, Name={1}, Enabled={2}, Target={3}, SearchText='{4}', ReplacementText='{5}'",
r.Id, r.Name, r.IsEnabled, r.Target, r.SearchText, r.ReplacementText)))));
var fileNameRules = executionRuleFile.Rules
.Where(r => r != null && r.IsEnabled && !string.IsNullOrEmpty(r.SearchText) && r.Target == RuleTarget.FileName)
.ToList();
if (fileNameRules.Count > 0)
{
var sourceFileName = Path.GetFileNameWithoutExtension(item.SourcePath);
var extension = Path.GetExtension(item.SourcePath);
_logService.Info("FILENAME_RULES", "应用文件名替换规则。",
string.Format("源文件名={0}; 规则数={1}; 规则详情=[{2}]",
sourceFileName,
fileNameRules.Count,
string.Join("; ", fileNameRules.Select(r =>
string.Format("Id={0}, SearchText='{1}', ReplacementText='{2}', MatchMode={3}, CaseSensitive={4}, WholeWord={5}",
r.Id, r.SearchText, r.ReplacementText, r.MatchMode, r.IsCaseSensitive, r.IsWholeWord)))));
var replacedName = FileScanService.ApplyRulesToFileName(sourceFileName, fileNameRules);
_logService.Info("FILENAME_REPLACED", "文件名替换结果。",
string.Format("原文件名={0}; 替换后={1}", sourceFileName, replacedName));
var outputDir = Path.GetDirectoryName(item.OutputPath) ?? string.Empty;
var diffDir = Path.GetDirectoryName(item.DiffPath) ?? string.Empty;
var datedSuffix = "-" + DateTime.Today.ToString("yyyyMMdd");
item.OutputPath = Path.Combine(outputDir, replacedName + datedSuffix + extension);
item.DiffPath = Path.Combine(diffDir, replacedName + datedSuffix + ".diff");
}
var startDetail = await Task.Run(() => BuildReplaceFileStartDetail(item, displayIndex, selectedItems.Count));
_logService.Info("REPLACE_FILE_START", "开始处理替换文件。", startDetail);
try
{
var progressThrottle = new RuntimeProgressThrottle();
var progress = new Progress<FileProcessProgress>(value =>
{
LogProcessProgressWarning("REPLACE_PROGRESS_WARN", value);
var forceRefresh = value.FilePercent >= 100;
if (!progressThrottle.ShouldApply(forceRefresh))
{
return;
}
UpdateReplaceStatus(
displayIndex,
selectedItems.Count,
value.CurrentRuleName ?? value.CurrentRuleId ?? "-",
value.CurrentFileTextCount,
totalHits + value.CurrentFileTextCount,
value.FilePercent,
forceRefresh);
});
var request = new ReplaceFileRequest
{
SourcePath = item.SourcePath,
OutputPath = item.OutputPath,
DiffPath = item.DiffPath,
SourceRootDirectory = _txtReplaceSource.Text.Trim(),
RuleFilePath = _txtRuleFile.Text.Trim(),
RuleFile = executionRuleFile,
EncryptDiff = encryptDiff,
EnableImageReplacement = _chkEnableImageReplacement.Checked,
FixedImageSeed = fixedImageSeed,
};
var result = await Task.Run(() => _documentProcessingService.Replace(request, progress, CancellationToken.None));
item.OutputPath = result.OutputPath;
item.DiffPath = result.DiffPath;
item.TextOperationCount = result.TextOperationCount;
item.ImageOperationCount = result.ImageOperationCount;
item.Status = ProcessingStatus.Success;
RefreshReplaceItemRow(item, forceRefresh: true);
successCount++;
totalHits += result.TextOperationCount + result.ImageOperationCount;
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true);
var doneDetail = await Task.Run(() => BuildReplaceFileDoneDetail(item, result, displayIndex, selectedItems.Count));
_logService.Info(
"REPLACE_FILE_DONE",
string.Format("文件替换完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount),
doneDetail);
}
catch (Exception ex)
{
item.Status = ProcessingStatus.Failed;
RefreshReplaceItemRow(item, forceRefresh: true);
failedCount++;
_replaceFileProgress.Value = 0;
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
var failDetail = await Task.Run(() => BuildReplaceFileFailDetail(item, ex, displayIndex, selectedItems.Count));
_logService.Error("REPLACE_FILE_FAIL", "替换文件处理失败。", failDetail);
}
completedCount++;
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
FlushReplaceRuntimeUi(forceRefresh: true);
}
}
var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1))
.Select(_ => RunWorkerAsync())
.ToArray();
await Task.WhenAll(workers);
if (_replaceExecutionCancellation.IsCancellationRequested)
{
stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress));
completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount);
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
FlushControl(_gridReplace, forceRefresh: true);
FlushReplaceRuntimeUi(forceRefresh: true);
}
}
finally
{
_isReplaceRunning = false;
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
_btnStopReplace.Enabled = false;
SetRuleEditingEnabled(true);
UpdateActionStates();
}
// 批次里每个文档的 DiffPayload含所有图片 base64是大块瞬态分配GC 不保证即时回收。
// 在弹完成对话框、触发自动恢复扫描之前主动回收一次,把 ~4GB 还给系统,避免残留内存压力拖慢后续 UI。
await Task.Run(() =>
{
GC.Collect();
GC.WaitForPendingFinalizers();
});
CompleteReplaceRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits);
var summary = string.Format("替换完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits);
_logService.Info("REPLACE_DONE", summary);
ShowMessage(summary, "开始替换", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
await RefreshRestoreScanAsync();
}
private async Task ExecuteRestoreBatchAsync(IList<FileScanItem> selectedItems)
{
_restoreExecutionCancellation?.Dispose();
_restoreExecutionCancellation = new CancellationTokenSource();
_isRestoreRunning = true;
SetRuleEditingEnabled(false);
foreach (var item in selectedItems)
{
item.Status = ProcessingStatus.Pending;
item.TextOperationCount = 0;
item.ImageOperationCount = 0;
}
ResetRestoreProgress(selectedItems.Count);
UpdateActionStates();
var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount);
var executionItems = plan.ExecutionItems.ToList();
var displayOrder = executionItems
.Select((item, index) => new { item, index = index + 1 })
.ToDictionary(entry => entry.item, entry => entry.index);
var successCount = 0;
var failedCount = 0;
var stoppedCount = 0;
var totalHits = 0;
var completedCount = 0;
var nextExecutionIndex = -1;
foreach (var skipped in plan.SkippedItems)
{
skipped.Item.Status = ProcessingStatus.Failed;
RefreshRestoreItemRow(skipped.Item, forceRefresh: true);
failedCount++;
completedCount++;
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
FlushRestoreRuntimeUi(forceRefresh: true);
_logService.Warning(
"RESTORE_BATCH_SKIP",
skipped.Message,
string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath));
}
try
{
_logService.Info(
"RESTORE_BATCH_PLAN",
string.Format(
"恢复批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。",
selectedItems.Count,
executionItems.Count,
plan.SkippedItems.Count,
plan.DegreeOfParallelism,
plan.RequiresSerialExecution ? "(包含 .doc已退回串行" : string.Empty),
BuildRestoreBatchPlanDetail(selectedItems, plan));
async Task RunWorkerAsync()
{
while (true)
{
if (_restoreExecutionCancellation.IsCancellationRequested)
{
return;
}
var executionIndex = Interlocked.Increment(ref nextExecutionIndex);
if (executionIndex >= executionItems.Count)
{
return;
}
var item = executionItems[executionIndex];
var displayIndex = displayOrder[item];
item.Status = ProcessingStatus.InProgress;
RefreshRestoreItemRow(item, forceRefresh: true);
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
var startDetail = await Task.Run(() => BuildRestoreFileStartDetail(item, displayIndex, selectedItems.Count));
_logService.Info("RESTORE_FILE_START", "开始处理恢复文件。", startDetail);
try
{
var progressThrottle = new RuntimeProgressThrottle();
var progress = new Progress<FileProcessProgress>(value =>
{
LogProcessProgressWarning("RESTORE_PROGRESS_WARN", value);
var forceRefresh = value.FilePercent >= 100;
if (!progressThrottle.ShouldApply(forceRefresh))
{
return;
}
UpdateRestoreStatus(
displayIndex,
selectedItems.Count,
value.CurrentRuleName ?? value.CurrentRuleId ?? "-",
value.CurrentFileTextCount,
totalHits + value.CurrentFileTextCount,
value.FilePercent,
forceRefresh);
});
var request = new RestoreFileRequest
{
ReplacedPath = item.SourcePath,
DiffPath = item.DiffPath,
OutputPath = item.OutputPath,
ReplaceRootDirectory = _txtRestoreSource.Text.Trim(),
CurrentRuleFilePath = _txtRuleFile.Text.Trim(),
};
var result = await Task.Run(() => _documentProcessingService.Restore(request, progress, CancellationToken.None));
item.OutputPath = result.OutputPath;
item.TextOperationCount = result.TextOperationCount;
item.ImageOperationCount = result.ImageOperationCount;
item.Status = ProcessingStatus.Success;
RefreshRestoreItemRow(item, forceRefresh: true);
successCount++;
totalHits += result.TextOperationCount + result.ImageOperationCount;
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true);
var doneDetail = await Task.Run(() => BuildRestoreFileDoneDetail(item, result, displayIndex, selectedItems.Count));
_logService.Info(
"RESTORE_FILE_DONE",
string.Format("文件恢复完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount),
doneDetail);
}
catch (Exception ex)
{
item.Status = ProcessingStatus.Failed;
RefreshRestoreItemRow(item, forceRefresh: true);
failedCount++;
_restoreFileProgress.Value = 0;
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
var failDetail = await Task.Run(() => BuildRestoreFileFailDetail(item, ex, displayIndex, selectedItems.Count));
_logService.Error("RESTORE_FILE_FAIL", "恢复文件处理失败。", failDetail);
}
completedCount++;
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
FlushRestoreRuntimeUi(forceRefresh: true);
}
}
var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1))
.Select(_ => RunWorkerAsync())
.ToArray();
await Task.WhenAll(workers);
if (_restoreExecutionCancellation.IsCancellationRequested)
{
stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress));
completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount);
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
FlushControl(_gridRestore, forceRefresh: true);
FlushRestoreRuntimeUi(forceRefresh: true);
}
}
finally
{
_isRestoreRunning = false;
_restoreFileProgress.Style = ProgressBarStyle.Blocks;
_btnStopRestore.Enabled = false;
SetRuleEditingEnabled(true);
UpdateActionStates();
}
// 恢复批次同样释放掉 DiffPayload 载荷,避免残留内存影响后续交互。
await Task.Run(() =>
{
GC.Collect();
GC.WaitForPendingFinalizers();
});
CompleteRestoreRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits);
var summary = string.Format("恢复完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits);
_logService.Info("RESTORE_DONE", summary);
ShowMessage(summary, "开始恢复", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
}
private bool PrepareRuleExecution(out RuleFile executionRuleFile)
{
executionRuleFile = CreateExecutionRuleFile();
var report = _ruleService.Validate(executionRuleFile);
foreach (var issue in report.Issues)
{
LogRuleIssue(issue);
}
if (report.Issues.Count == 0)
{
return true;
}
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?";
return ShowMessage(
prompt,
"规则执行检查",
MessageBoxButtons.YesNo,
report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information) == DialogResult.Yes;
}
private bool PrepareRuleExecutionWithBlockingErrors(out RuleFile executionRuleFile)
{
executionRuleFile = CreateExecutionRuleFile();
var report = _ruleService.Validate(executionRuleFile);
foreach (var issue in report.Issues)
{
LogRuleIssue(issue);
}
if (report.Issues.Count == 0)
{
return true;
}
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
if (report.HasErrors)
{
ShowMessage(
summary + Environment.NewLine + Environment.NewLine + "存在错误,必须先修复后才能执行。",
"规则执行检查",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return false;
}
var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?";
return ShowMessage(
prompt,
"规则执行检查",
MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes;
}
private RuleFile CreateExecutionRuleFile()
{
if (_gridRules.IsHandleCreated)
{
_gridRules.EndEdit();
}
var result = new RuleFile
{
Format = _ruleFile.Format,
Version = _ruleFile.Version,
};
foreach (var rule in _rules)
{
result.Rules.Add(new RuleDefinition
{
Id = rule.Id,
Name = rule.Name,
IsEnabled = rule.IsEnabled,
MatchMode = rule.MatchMode,
Target = rule.Target,
IsCaseSensitive = rule.IsCaseSensitive,
IsWholeWord = rule.IsWholeWord,
SearchText = rule.SearchText,
ReplacementText = rule.ReplacementText,
Note = rule.Note,
});
}
_ruleFile = result;
return result;
}
private bool TryPersistConfigFromUi()
{
try
{
CaptureUiToConfig();
_configurationService.Save(_config);
return true;
}
catch (Exception ex)
{
_logService.Error("CONFIG_SAVE_FAIL", "保存当前配置失败。", ex.Message);
ShowMessage("保存当前配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
private void ResetReplaceProgress(int totalCount)
{
_replaceTotalProgress.Style = ProgressBarStyle.Blocks;
_replaceTotalProgress.Maximum = Math.Max(totalCount, 1);
_replaceTotalProgress.Value = 0;
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
_replaceFileProgress.Value = 0;
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
FlushReplaceRuntimeUi(forceRefresh: true);
}
private void ResetRestoreProgress(int totalCount)
{
_restoreTotalProgress.Style = ProgressBarStyle.Blocks;
_restoreTotalProgress.Maximum = Math.Max(totalCount, 1);
_restoreTotalProgress.Value = 0;
_restoreFileProgress.Style = ProgressBarStyle.Blocks;
_restoreFileProgress.Value = 0;
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
FlushRestoreRuntimeUi(forceRefresh: true);
}
private void LogProcessProgressWarning(string eventCode, FileProcessProgress value)
{
if (value == null || !value.IsWarning || string.IsNullOrWhiteSpace(value.Message))
{
return;
}
var detail = string.IsNullOrWhiteSpace(value.FilePath) ? null : "文件: " + value.FilePath;
_logService.Warning(eventCode, value.Message, detail);
}
private void UpdateReplaceStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false)
{
_replaceFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100));
_lblReplaceRuntime.Text = string.Format(
"当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}",
currentFile,
totalFiles,
string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule,
currentFileHits,
totalHits);
FlushReplaceRuntimeUi(forceRefresh);
}
private void CompleteReplaceRuntimeStatus(int completedFiles, int totalFiles, int totalHits)
{
var safeTotal = Math.Max(0, totalFiles);
var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal));
_replaceTotalProgress.Value = Math.Min(safeCompleted, _replaceTotalProgress.Maximum);
UpdateReplaceStatus(
safeCompleted,
safeTotal,
"-",
0,
totalHits,
safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0,
forceRefresh: true);
}
private void UpdateRestoreStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false)
{
_restoreFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100));
_lblRestoreRuntime.Text = string.Format(
"当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}",
currentFile,
totalFiles,
string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule,
currentFileHits,
totalHits);
FlushRestoreRuntimeUi(forceRefresh);
}
private void CompleteRestoreRuntimeStatus(int completedFiles, int totalFiles, int totalHits)
{
var safeTotal = Math.Max(0, totalFiles);
var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal));
_restoreTotalProgress.Value = Math.Min(safeCompleted, _restoreTotalProgress.Maximum);
UpdateRestoreStatus(
safeCompleted,
safeTotal,
"-",
0,
totalHits,
safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0,
forceRefresh: true);
}
private void FlushReplaceRuntimeUi(bool forceRefresh)
{
if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastReplaceRuntimeFlushTick))
{
return;
}
FlushControl(_replaceFileProgress, forceRefresh);
FlushControl(_replaceTotalProgress, forceRefresh);
FlushControl(_lblReplaceRuntime, forceRefresh);
}
private void FlushRestoreRuntimeUi(bool forceRefresh)
{
if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastRestoreRuntimeFlushTick))
{
return;
}
FlushControl(_restoreFileProgress, forceRefresh);
FlushControl(_restoreTotalProgress, forceRefresh);
FlushControl(_lblRestoreRuntime, forceRefresh);
}
private static bool ShouldRefreshRuntimeUi(ref int lastFlushTick)
{
var now = Environment.TickCount & int.MaxValue;
if (lastFlushTick != 0 && now >= lastFlushTick && now - lastFlushTick < RuntimeUiRefreshIntervalMilliseconds)
{
return false;
}
lastFlushTick = now;
return true;
}
private static bool ShouldApplyProgressUpdate(ref int lastProgressTick, bool forceRefresh)
{
if (forceRefresh)
{
lastProgressTick = Environment.TickCount & int.MaxValue;
return true;
}
return ShouldRefreshRuntimeUi(ref lastProgressTick);
}
private static void FlushControl(Control control, bool forceRefresh)
{
if (control == null || control.IsDisposed || !control.IsHandleCreated)
{
return;
}
if (forceRefresh)
{
control.Refresh();
return;
}
control.Update();
}
private void RefreshReplaceItemRow(FileScanItem item, bool forceRefresh = false)
{
RefreshFileItemRow(_gridReplace, _replaceItems, item, forceRefresh);
}
private void RefreshRestoreItemRow(FileScanItem item, bool forceRefresh = false)
{
RefreshFileItemRow(_gridRestore, _restoreItems, item, forceRefresh);
}
private static void RefreshFileItemRow(DataGridView grid, BindingList<FileScanItem> items, FileScanItem item, bool forceRefresh)
{
if (grid == null || items == null || item == null || grid.IsDisposed || !grid.IsHandleCreated)
{
return;
}
var rowIndex = items.IndexOf(item);
if (rowIndex >= 0 && rowIndex < grid.Rows.Count)
{
grid.InvalidateRow(rowIndex);
}
FlushControl(grid, forceRefresh);
}
private sealed class RuntimeProgressThrottle
{
private int _lastProgressTick;
public bool ShouldApply(bool forceRefresh)
{
return ShouldApplyProgressUpdate(ref _lastProgressTick, forceRefresh);
}
}
private static int MarkRemainingItemsStopped(IEnumerable<FileScanItem> items)
{
var count = 0;
foreach (var item in items)
{
if (item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress)
{
item.Status = ProcessingStatus.Stopped;
count++;
}
}
return count;
}
private string GetSelectedRestoreDiffInputFormat()
{
return string.Equals(_cmbRestoreDiffInput.SelectedItem as string, ".bmp", StringComparison.OrdinalIgnoreCase)
? ".bmp"
: ".diff";
}
private void SetRestoreDiffInputFormat(string diffInputFormat)
{
var normalized = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase)
? ".bmp"
: ".diff";
_cmbRestoreDiffInput.SelectedItem = normalized;
}
private void LoadRuleFile(string filePath)
{
try
{
_ruleFile = _ruleService.Load(filePath);
ReplaceBinding(_rules, _ruleFile.Rules);
_logService.Info("RULE_LOAD", "规则文件已加载。", filePath);
UpdateActionStates();
}
catch (Exception ex)
{
_logService.Error("RULE_LOAD_FAIL", "加载规则文件失败。", ex.Message);
ShowMessage("加载规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task BrowseFolderIntoTextBoxAsync(TextBox textBox, Func<Task> refreshAsync)
{
string selectedPath;
if (!ModernFolderPicker.TrySelectFolder(this, textBox.Text, out selectedPath))
{
return;
}
await ApplySelectedFolderPathAsync(textBox, selectedPath, refreshAsync);
}
private async Task ApplySelectedFolderPathAsync(TextBox textBox, string selectedPath, Func<Task> refreshAsync)
{
textBox.Text = selectedPath ?? string.Empty;
if (refreshAsync != null)
{
await refreshAsync();
}
}
private string GetReplacePreviewOutputDirectory(string sourceDirectory)
{
var output = _txtReplaceOutput.Text.Trim();
if (string.IsNullOrWhiteSpace(output))
{
return string.Empty;
}
var validationMessages = _fileScanService.ValidateReplaceDirectories(sourceDirectory, output);
return validationMessages.Count == 0 ? output : string.Empty;
}
private string GetRestorePreviewOutputDirectory(string replaceDirectory)
{
var output = _txtRestoreOutput.Text.Trim();
if (string.IsNullOrWhiteSpace(output))
{
return string.Empty;
}
var validationMessages = _fileScanService.ValidateRestoreDirectories(replaceDirectory, output);
return validationMessages.Count == 0 ? output : string.Empty;
}
private IList<string> GetReplaceExecutionValidationMessages()
{
var source = _txtReplaceSource.Text.Trim();
var output = _txtReplaceOutput.Text.Trim();
var messages = new List<string>();
if (string.IsNullOrWhiteSpace(source))
{
messages.Add("未选择原始文件夹。");
}
else if (!Directory.Exists(source))
{
messages.Add("原始文件夹不存在。");
}
if (string.IsNullOrWhiteSpace(output))
{
messages.Add("未选择替换文件夹。");
}
messages.AddRange(_fileScanService.ValidateReplaceDirectories(source, output));
return messages;
}
private IList<string> GetRestoreExecutionValidationMessages()
{
var source = _txtRestoreSource.Text.Trim();
var output = _txtRestoreOutput.Text.Trim();
var messages = new List<string>();
if (string.IsNullOrWhiteSpace(source))
{
messages.Add("未选择替换文件夹。");
}
else if (!Directory.Exists(source))
{
messages.Add("替换文件夹不存在。");
}
if (string.IsNullOrWhiteSpace(output))
{
messages.Add("未选择恢复文件夹。");
}
messages.AddRange(_fileScanService.ValidateRestoreDirectories(source, output));
return messages;
}
private void UpdateActionStates()
{
var hasReplaceSelection = _replaceItems.Any(item => item.IsSelected);
var hasRestoreSelection = _restoreItems.Any(item => item.IsSelected);
var busy = _isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning;
var canEditReplacePage = !busy;
var canEditRestorePage = !busy;
var canEditRules = !busy && _gridRules.Enabled;
var canExecuteReplace = !busy && hasReplaceSelection && GetReplaceExecutionValidationMessages().Count == 0;
var canExecuteRestore = !busy && hasRestoreSelection && GetRestoreExecutionValidationMessages().Count == 0;
_btnAutoExtract.Enabled = !_isAutoExtractRunning && !_isReplaceRunning && !_isRestoreRunning && hasReplaceSelection;
_btnStartReplace.Enabled = canExecuteReplace;
_btnStopReplace.Enabled = _isAutoExtractRunning || _isReplaceRunning;
_btnStartRestore.Enabled = canExecuteRestore;
_btnStopRestore.Enabled = _isRestoreRunning;
_txtReplaceSource.Enabled = canEditReplacePage;
_txtReplaceOutput.Enabled = canEditReplacePage;
_btnBrowseReplaceSource.Enabled = canEditReplacePage;
_btnBrowseReplaceOutput.Enabled = canEditReplacePage;
_btnSelectAllReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0;
_btnClearReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0;
_gridReplace.ReadOnly = busy;
_txtRestoreSource.Enabled = canEditRestorePage;
_txtRestoreOutput.Enabled = canEditRestorePage;
_cmbRestoreDiffInput.Enabled = canEditRestorePage;
_btnBrowseRestoreSource.Enabled = canEditRestorePage;
_btnBrowseRestoreOutput.Enabled = canEditRestorePage;
_btnSelectAllRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0;
_btnClearRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0;
_gridRestore.ReadOnly = busy;
_gridRules.Enabled = canEditRules;
_txtRuleFile.Enabled = canEditRules;
_btnOpenRules.Enabled = canEditRules;
_btnSaveRules.Enabled = canEditRules;
_btnUndoAutoExtract.Enabled = canEditRules && _lastAutoExtractRuleIds.Count > 0;
}
private void SetRuleEditingEnabled(bool enabled)
{
_gridRules.Enabled = enabled;
_txtRuleFile.Enabled = enabled;
UpdateActionStates();
}
private void LogRuleIssue(RuleValidationIssue issue)
{
var formatted = _ruleService.FormatIssue(issue);
if (issue.Severity == RuleValidationSeverity.Error)
{
_logService.Error("RULE_CHECK", formatted);
}
else if (issue.Severity == RuleValidationSeverity.Warning)
{
_logService.Warning("RULE_CHECK", formatted);
}
else
{
_logService.Info("RULE_CHECK", formatted);
}
}
private void SetSelection(IList<FileScanItem> items, bool isSelected)
{
foreach (var item in items)
{
item.IsSelected = isSelected;
}
UpdateActionStates();
}
private void SetSelectedFileItems(DataGridView grid, bool isSelected)
{
foreach (var item in GetSelectedDataBoundItems<FileScanItem>(grid))
{
item.IsSelected = isSelected;
}
grid.Refresh();
UpdateActionStates();
}
private void DeleteSelectedFileItems(BindingList<FileScanItem> items, DataGridView grid)
{
foreach (var item in GetSelectedDataBoundItems<FileScanItem>(grid))
{
items.Remove(item);
}
UpdateActionStates();
}
private void SetSelectedRulesEnabled(bool isEnabled)
{
foreach (var rule in GetSelectedDataBoundItems<RuleDefinition>(_gridRules))
{
rule.IsEnabled = isEnabled;
}
RefreshRuleState();
}
private void DeleteSelectedRules()
{
foreach (var rule in GetSelectedDataBoundItems<RuleDefinition>(_gridRules))
{
_rules.Remove(rule);
}
RefreshRuleState();
}
private void RefreshRuleState()
{
_ruleFile.Rules = _rules.ToList();
var existingRuleIds = new HashSet<string>(
_rules.Where(item => !string.IsNullOrWhiteSpace(item.Id)).Select(item => item.Id),
StringComparer.OrdinalIgnoreCase);
_lastAutoExtractRuleIds = _lastAutoExtractRuleIds.Where(existingRuleIds.Contains).ToList();
_gridRules.Refresh();
UpdateActionStates();
}
private static List<T> GetSelectedDataBoundItems<T>(DataGridView grid)
where T : class
{
return grid.SelectedRows
.Cast<DataGridViewRow>()
.OrderBy(row => row.Index)
.Select(row => row.DataBoundItem as T)
.Where(item => item != null)
.ToList();
}
private static List<int> NormalizeIndexes(IEnumerable<int> rowIndexes, int count)
{
return (rowIndexes ?? Array.Empty<int>())
.Where(index => index >= 0 && index < count)
.Distinct()
.OrderBy(index => index)
.ToList();
}
private static void ReplaceBinding<T>(BindingList<T> target, IEnumerable<T> source)
{
target.Clear();
foreach (var item in source)
{
target.Add(item);
}
}
private static void ConfigureNumeric(NumericUpDown numeric, int minimum, int maximum)
{
numeric.Minimum = minimum;
numeric.Maximum = maximum;
numeric.Width = 120;
}
private static Control CreateProgressLine(string labelText, ProgressBar progressBar)
{
var panel = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 2,
AutoSize = true,
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90F));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
panel.Controls.Add(new Label { Text = labelText, Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0);
progressBar.Dock = DockStyle.Fill;
progressBar.Maximum = 100;
panel.Controls.Add(progressBar, 1, 0);
return panel;
}
private static void AddLabeledRow(
TableLayoutPanel layout,
int rowIndex,
string leftLabel,
Control leftTextBox,
Control leftButton,
string rightLabel,
Control rightTextBox,
Control rightButton)
{
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.Controls.Add(new Label { Text = leftLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 0, rowIndex);
leftTextBox.Dock = DockStyle.Fill;
layout.Controls.Add(leftTextBox, 1, rowIndex);
layout.Controls.Add(leftButton, 2, rowIndex);
layout.Controls.Add(new Label { Text = rightLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 3, rowIndex);
rightTextBox.Dock = DockStyle.Fill;
layout.Controls.Add(rightTextBox, 4, rowIndex);
layout.Controls.Add(rightButton, 5, rowIndex);
}
private static void AddSettingRow(TableLayoutPanel layout, int rowIndex, Control leftControl, Control rightControl)
{
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
leftControl.Dock = DockStyle.Fill;
rightControl.Dock = DockStyle.Fill;
layout.Controls.Add(leftControl, 0, rowIndex);
layout.Controls.Add(rightControl, 1, rowIndex);
}
private void ConfigureFileGrid(DataGridView grid, bool isRestoreGrid)
{
grid.AllowUserToAddRows = false;
grid.AllowUserToDeleteRows = false;
grid.AutoGenerateColumns = false;
grid.MultiSelect = true;
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
grid.EditMode = DataGridViewEditMode.EditOnEnter;
grid.Columns.Clear();
var checkColumn = new DataGridViewCheckBoxColumn
{
DataPropertyName = nameof(FileScanItem.IsSelected),
HeaderText = "勾选",
Width = 60,
};
grid.Columns.Add(checkColumn);
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.SourcePath),
HeaderText = isRestoreGrid ? "替换后文件路径" : "原始文件路径",
Width = 360,
ReadOnly = true,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.OutputPath),
HeaderText = isRestoreGrid ? "恢复文件路径" : "替换后文件路径",
Width = 360,
ReadOnly = true,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.DiffPath),
HeaderText = "差异文件路径",
Width = 320,
ReadOnly = true,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.TextOperationCount),
HeaderText = isRestoreGrid ? "文本恢复次数" : "文本替换次数",
Width = 110,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.ImageOperationCount),
HeaderText = isRestoreGrid ? "图片恢复次数" : "图片替换次数",
Width = 110,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(FileScanItem.Status),
HeaderText = "当前状态",
Width = 90,
ReadOnly = true,
});
grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged;
grid.CellValueChanged += GridSelection_CellValueChanged;
grid.CellMouseDown += Grid_CellMouseDownSelectRow;
grid.RowPrePaint += Grid_RowPrePaint;
}
private void ConfigureRuleGrid(DataGridView grid)
{
grid.AllowUserToAddRows = false;
grid.AllowUserToDeleteRows = false;
grid.AutoGenerateColumns = false;
grid.MultiSelect = true;
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
grid.EditMode = DataGridViewEditMode.EditOnEnter;
grid.Columns.Clear();
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Id), HeaderText = "规则 ID", Width = 110 });
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Name), HeaderText = "规则名称", Width = 140 });
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsEnabled), HeaderText = "启用", Width = 60 });
grid.Columns.Add(new DataGridViewComboBoxColumn
{
DataPropertyName = nameof(RuleDefinition.MatchMode),
HeaderText = "匹配模式",
Width = 110,
DataSource = Enum.GetValues(typeof(RuleMatchMode)),
});
grid.Columns.Add(new DataGridViewComboBoxColumn
{
DataPropertyName = nameof(RuleDefinition.Target),
HeaderText = "目标类型",
Width = 100,
DataSource = Enum.GetValues(typeof(RuleTarget)),
ValueType = typeof(RuleTarget),
});
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsCaseSensitive), HeaderText = "区分大小写", Width = 90 });
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsWholeWord), HeaderText = "全字匹配", Width = 90 });
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.SearchText), HeaderText = "原文本/表达式", Width = 200 });
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.ReplacementText), HeaderText = "新文本/模板", Width = 180 });
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Note), HeaderText = "备注", Width = 320 });
grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged;
grid.CellValueChanged += GridSelection_CellValueChanged;
grid.CellMouseDown += Grid_CellMouseDownSelectRow;
grid.DataError += delegate { };
}
private void Grid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
var grid = sender as DataGridView;
if (grid != null && grid.IsCurrentCellDirty && grid.CurrentCell is DataGridViewCheckBoxCell)
{
grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
private void GridSelection_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
UpdateActionStates();
}
private void Grid_CellMouseDownSelectRow(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Button != MouseButtons.Right || e.RowIndex < 0)
{
return;
}
var grid = sender as DataGridView;
if (grid == null || e.RowIndex >= grid.Rows.Count)
{
return;
}
var row = grid.Rows[e.RowIndex];
if (!row.Selected)
{
grid.ClearSelection();
row.Selected = true;
}
if (e.ColumnIndex >= 0 && e.ColumnIndex < row.Cells.Count)
{
grid.CurrentCell = row.Cells[e.ColumnIndex];
}
}
private void Grid_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
{
var grid = sender as DataGridView;
if (grid == null || e.RowIndex < 0 || e.RowIndex >= grid.Rows.Count)
{
return;
}
var item = grid.Rows[e.RowIndex].DataBoundItem as FileScanItem;
if (item == null)
{
return;
}
var color = Color.Black;
switch (item.Status)
{
case ProcessingStatus.Success:
color = Color.Green;
break;
case ProcessingStatus.Failed:
color = Color.Red;
break;
case ProcessingStatus.InProgress:
color = Color.Blue;
break;
case ProcessingStatus.Stopped:
color = Color.Gray;
break;
}
grid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = color;
}
private DialogResult ShowMessage(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{
return _dialogService.ShowMessage(this, text, caption, buttons, icon);
}
internal Task ExecuteReplaceBatchForTestAsync(IList<FileScanItem> selectedItems, RuleFile executionRuleFile, string sourceRootDirectory, string ruleFilePath, bool encryptDiff, int? fixedImageSeed)
{
_txtReplaceSource.Text = sourceRootDirectory ?? string.Empty;
_txtRuleFile.Text = ruleFilePath ?? string.Empty;
_chkJsonDiffEncryption.Checked = encryptDiff;
_chkUseFixedImageSeed.Checked = fixedImageSeed.HasValue;
_numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, fixedImageSeed ?? 0));
return ExecuteReplaceBatchAsync(selectedItems, executionRuleFile);
}
internal Task ExecuteRestoreBatchForTestAsync(IList<FileScanItem> selectedItems, string replaceRootDirectory, string ruleFilePath)
{
_txtRestoreSource.Text = replaceRootDirectory ?? string.Empty;
_txtRuleFile.Text = ruleFilePath ?? string.Empty;
return ExecuteRestoreBatchAsync(selectedItems);
}
internal Task RefreshReplaceScanForTestAsync(string sourceDirectory, string outputDirectory)
{
_txtReplaceSource.Text = sourceDirectory ?? string.Empty;
_txtReplaceOutput.Text = outputDirectory ?? string.Empty;
return RefreshReplaceScanAsync();
}
internal Task ApplyReplaceSourceSelectionForTestAsync(string selectedPath)
{
return ApplySelectedFolderPathAsync(_txtReplaceSource, selectedPath, RefreshReplaceScanAsync);
}
internal Task ApplyReplaceOutputSelectionForTestAsync(string selectedPath)
{
return ApplySelectedFolderPathAsync(_txtReplaceOutput, selectedPath, RefreshReplaceScanAsync);
}
internal Task RefreshRestoreScanForTestAsync(string sourceDirectory, string outputDirectory, string diffInputFormat)
{
_txtRestoreSource.Text = sourceDirectory ?? string.Empty;
_txtRestoreOutput.Text = outputDirectory ?? string.Empty;
SetRestoreDiffInputFormat(diffInputFormat);
return RefreshRestoreScanAsync();
}
internal Task ApplyRestoreSourceSelectionForTestAsync(string selectedPath)
{
return ApplySelectedFolderPathAsync(_txtRestoreSource, selectedPath, RefreshRestoreScanAsync);
}
internal Task ApplyRestoreOutputSelectionForTestAsync(string selectedPath)
{
return ApplySelectedFolderPathAsync(_txtRestoreOutput, selectedPath, RefreshRestoreScanAsync);
}
internal Task SetRestoreDiffInputForTestAsync(string diffInputFormat)
{
SetRestoreDiffInputFormat(diffInputFormat);
return RefreshRestoreScanAsync();
}
internal Task RunAutoExtractForTestAsync(IList<FileScanItem> replaceItems, IList<RuleDefinition> existingRules, AutoExtractSettings settings)
{
ReplaceBinding(_replaceItems, replaceItems ?? Array.Empty<FileScanItem>());
ReplaceBinding(_rules, existingRules ?? Array.Empty<RuleDefinition>());
_ruleFile.Rules = _rules.ToList();
if (settings != null)
{
_config.Settings.AutoExtract = settings;
ApplyConfigToUi();
}
UpdateActionStates();
return RunAutoExtractAsync();
}
internal void RequestStopReplaceForTest()
{
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
{
_replaceExecutionCancellation.Cancel();
}
}
internal void RequestStopRestoreForTest()
{
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
{
_restoreExecutionCancellation.Cancel();
}
}
internal void RaiseLogWriteFailureForTest(LogWriteFailureEventArgs e)
{
LogService_WriteFailed(this, e);
}
internal void UndoAutoExtractForTest()
{
UndoAutoExtract_Click(this, EventArgs.Empty);
}
internal void LoadReplaceItemsForTest(IList<FileScanItem> items)
{
ReplaceBinding(_replaceItems, items ?? Array.Empty<FileScanItem>());
UpdateActionStates();
}
internal void LoadRestoreItemsForTest(IList<FileScanItem> items)
{
ReplaceBinding(_restoreItems, items ?? Array.Empty<FileScanItem>());
UpdateActionStates();
}
internal void LoadRulesForTest(IList<RuleDefinition> rules)
{
ReplaceBinding(_rules, rules ?? Array.Empty<RuleDefinition>());
RefreshRuleState();
}
internal IList<string> LogContextMenuItemsForTest
{
get { return GetContextMenuTexts(_logBox.ContextMenuStrip); }
}
internal IList<string> ReplaceContextMenuItemsForTest
{
get { return GetContextMenuTexts(_gridReplace.ContextMenuStrip); }
}
internal IList<string> RestoreContextMenuItemsForTest
{
get { return GetContextMenuTexts(_gridRestore.ContextMenuStrip); }
}
internal IList<string> RuleContextMenuItemsForTest
{
get { return GetContextMenuTexts(_gridRules.ContextMenuStrip); }
}
internal bool IsRuleEditingEnabledForTest
{
get { return _gridRules.Enabled; }
}
internal bool IsUndoAutoExtractEnabledForTest
{
get { return _btnUndoAutoExtract.Enabled; }
}
internal bool IsAutoExtractEnabledForTest
{
get { return _btnAutoExtract.Enabled; }
}
internal bool IsStartReplaceEnabledForTest
{
get { return _btnStartReplace.Enabled; }
}
internal bool IsStartRestoreEnabledForTest
{
get { return _btnStartRestore.Enabled; }
}
internal bool IsStopReplaceEnabledForTest
{
get { return _btnStopReplace.Enabled; }
}
internal bool IsStopRestoreEnabledForTest
{
get { return _btnStopRestore.Enabled; }
}
internal string ReplaceRuntimeTextForTest
{
get { return _lblReplaceRuntime.Text; }
}
internal string RestoreRuntimeTextForTest
{
get { return _lblRestoreRuntime.Text; }
}
internal string RestoreBrowseSourceButtonTextForTest
{
get { return _btnBrowseRestoreSource.Text; }
}
internal string RestoreBrowseOutputButtonTextForTest
{
get { return _btnBrowseRestoreOutput.Text; }
}
internal string RestoreSelectAllButtonTextForTest
{
get { return _btnSelectAllRestore.Text; }
}
internal string RestoreClearButtonTextForTest
{
get { return _btnClearRestore.Text; }
}
internal int SettingsRuleListWidthForTest
{
get
{
return _settingsRuleListSplitContainer == null
? 0
: _settingsRuleListSplitContainer.ClientSize.Width - _settingsRuleListSplitContainer.SplitterWidth - _settingsRuleListSplitContainer.SplitterDistance;
}
}
internal void ApplySettingsRuleListInitialWidthForTest(int width)
{
if (_settingsRuleListSplitContainer == null)
{
throw new InvalidOperationException("设置页规则列表分隔容器尚未初始化。");
}
_settingsRuleListInitialWidthApplied = false;
_settingsRuleListSplitContainer.Width = width;
ApplySettingsRuleListInitialWidth(_settingsRuleListSplitContainer);
}
internal string SelectedTabTextForTest
{
get { return _tabControl.SelectedTab == null ? string.Empty : _tabControl.SelectedTab.Text; }
}
internal int SelectedRuleIndexForTest
{
get
{
if (_gridRules.SelectedRows.Count > 0)
{
return _gridRules.SelectedRows[0].Index;
}
if (_gridRules.CurrentCell != null)
{
return _gridRules.CurrentCell.RowIndex;
}
return -1;
}
}
internal IList<RuleDefinition> RulesForTest
{
get { return _rules.Select(CloneRule).ToList(); }
}
internal IList<FileScanItem> ReplaceItemsForTest
{
get { return _replaceItems.Select(CloneFileScanItem).ToList(); }
}
internal IList<FileScanItem> RestoreItemsForTest
{
get { return _restoreItems.Select(CloneFileScanItem).ToList(); }
}
internal string LogTextForTest
{
get { return _logBox.Text; }
}
internal string LogCopyTextForTest
{
get { return GetAllLogText(); }
}
internal void AppendLogTextForTest(string text)
{
if (!string.IsNullOrEmpty(text))
{
_logBox.AppendText(text);
}
}
internal void ClearLogsForTest()
{
ClearLogBox();
}
internal void SetReplaceItemSelectionForTest(IEnumerable<int> rowIndexes, bool isSelected)
{
foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count))
{
_replaceItems[index].IsSelected = isSelected;
}
UpdateActionStates();
}
internal void DeleteReplaceItemsForTest(IEnumerable<int> rowIndexes)
{
foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count).OrderByDescending(item => item))
{
_replaceItems.RemoveAt(index);
}
UpdateActionStates();
}
internal void SetRestoreItemSelectionForTest(IEnumerable<int> rowIndexes, bool isSelected)
{
foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count))
{
_restoreItems[index].IsSelected = isSelected;
}
UpdateActionStates();
}
internal void DeleteRestoreItemsForTest(IEnumerable<int> rowIndexes)
{
foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count).OrderByDescending(item => item))
{
_restoreItems.RemoveAt(index);
}
UpdateActionStates();
}
internal void SetRuleEnabledForTest(IEnumerable<int> rowIndexes, bool isEnabled)
{
foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count))
{
_rules[index].IsEnabled = isEnabled;
}
RefreshRuleState();
}
internal void DeleteRulesForTest(IEnumerable<int> rowIndexes)
{
foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count).OrderByDescending(item => item))
{
_rules.RemoveAt(index);
}
RefreshRuleState();
}
private static FileScanItem CloneFileScanItem(FileScanItem source)
{
if (source == null)
{
return new FileScanItem();
}
return new FileScanItem
{
IsSelected = source.IsSelected,
SourcePath = source.SourcePath,
OutputPath = source.OutputPath,
DiffPath = source.DiffPath,
TextOperationCount = source.TextOperationCount,
ImageOperationCount = source.ImageOperationCount,
Status = source.Status,
};
}
private static RuleDefinition CloneRule(RuleDefinition source)
{
if (source == null)
{
return new RuleDefinition();
}
return new RuleDefinition
{
Id = source.Id,
Name = source.Name,
IsEnabled = source.IsEnabled,
MatchMode = source.MatchMode,
IsCaseSensitive = source.IsCaseSensitive,
IsWholeWord = source.IsWholeWord,
SearchText = source.SearchText,
ReplacementText = source.ReplacementText,
Note = source.Note,
};
}
private static List<string> GetContextMenuTexts(ContextMenuStrip contextMenu)
{
if (contextMenu == null)
{
return new List<string>();
}
return contextMenu.Items.Cast<ToolStripItem>().Select(item => item.Text).ToList();
}
}
}