Files
DCIT/DCIT.App/Forms/MainForm.cs

3380 lines
144 KiB
C#
Raw Normal View History

2026-07-13 10:04:36 +08:00
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();
}
}
}