using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; using System.IO; using System.Windows.Media.Imaging; using System.Windows; using System.Windows.Media; using System.Windows.Data; using System.Windows.Documents; using System.Text.RegularExpressions; using System.Xml; namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor { /// <summary> /// 工作区管理器,用于处理工作区管理器中文件、目录、资源文件条目的增、删、刷新状态等操作。 /// </summary> class WorkspaceManager { private XmlDocument workspaceItemsDocument = null; /// <summary> /// 此Xml文件用以管理工作区管理器中的各条目。 /// </summary> public XmlDocument WorkspaceItemsDocument { get { return workspaceItemsDocument; } } /// <summary> /// [构造方法] /// </summary> /// <param name="masterWindow">主窗口。</param> public WorkspaceManager(MainWindow masterWindow) { this.masterWindow = masterWindow; } private MainWindow masterWindow; /// <summary> /// 主窗口。 /// </summary> public MainWindow MasterWindow { get { return this.masterWindow; } } public static string WorkspaceTreeviewItemsXmlPath { get { var path = Globals.PathOfWorkspace; if (path.EndsWith("\\") == false) { path += "\\"; } return path += "WorkspaceItems.xml"; } } public void SaveWorkspaceTreeviewToXml() { if (Globals.MainWindow == null) return; var xmlFilePath = WorkspaceTreeviewItemsXmlPath; if (File.Exists(xmlFilePath)) { var bakXmlFilePath = xmlFilePath + ".bak"; if (File.Exists(bakXmlFilePath)) { File.Delete(bakXmlFilePath); } File.Copy(xmlFilePath, bakXmlFilePath); File.Delete(xmlFilePath); workspaceItemsDocument = null;//强制更新。 } var tv = Globals.MainWindow.tvWorkDirectory; if (tv.HasItems == false) return; var fstItem = tv.Items[0] as WorkspaceTreeViewItem; if (fstItem == null) return; if (File.Exists(xmlFilePath) == false) //这个不能加!!! // && workspaceItemsDocument == null) { XmlTextWriter xmlWriter = new XmlTextWriter(xmlFilePath, System.Text.Encoding.UTF8); xmlWriter.Formatting = Formatting.Indented; xmlWriter.WriteProcessingInstruction("xml", "version='1.0' encoding='UTF-8'"); xmlWriter.WriteStartElement("Item"); //如果像上面这样使用WriteProcessingInstruction, //这里就不使用WriteEndElement() //xmlWriter.WriteEndElement(); //这会导致<Root></Root>变成<Root /> xmlWriter.Close(); workspaceItemsDocument = new XmlDocument(); workspaceItemsDocument.Load(xmlFilePath); SaveWorkspaceTreeviewItemAsXmlElement(WorkspaceItemsDocument.DocumentElement, fstItem, FormatPath(new FileInfo(xmlFilePath).Directory.FullName)); workspaceItemsDocument.Save(xmlFilePath); } } /// <summary> /// 简单地保证提供的路径字串末尾必然有反斜杠字符。 /// </summary> /// <returns></returns> private static string FormatPath(string src) { if (src == null) return null; if (string.IsNullOrWhiteSpace(src)) return ""; if (src.EndsWith("\\") == false) return src + "\\"; return src; } private void SaveWorkspaceTreeviewItemAsXmlElement(XmlElement currentElement, WorkspaceTreeViewItem wtvi, string workspacePath) { if (currentElement == null || wtvi == null) return; var title = wtvi.Title; if (string.IsNullOrWhiteSpace(title)) { title = wtvi.ShortName; } currentElement.SetAttribute("Title", title); //之前使用绝对路径,如果用户在关闭工作区后手动移动了工作区目录的磁盘位置,会出错。 //现在改为使用相对路径。 var pathTail = wtvi.FullPath; if (pathTail.ToLower().StartsWith(workspacePath.ToLower())) { pathTail = pathTail.Substring(workspacePath.Length); } currentElement.SetAttribute("Path", pathTail); currentElement.SetAttribute("StatuHeader", wtvi.StatuHeader); currentElement.SetAttribute("StatuTail", wtvi.StatuTail); if (wtvi.IsExpanded) { currentElement.SetAttribute("IsExpanded", "true"); } else { currentElement.SetAttribute("IsExpanded", "false"); } if (wtvi.IsSelected) { currentElement.SetAttribute("IsSelected", "true"); } if (wtvi.ToolTip != null) { currentElement.SetAttribute("Tooltip", wtvi.ToolTip.ToString()); } currentElement.SetAttribute("ItemType", wtvi.ItemType.ToString()); if (wtvi.HasItems == false) return; foreach (var item in wtvi.Items) { var subwtvi = item as WorkspaceTreeViewItem; if (subwtvi == null) continue; var newXmlElement = currentElement.AppendXmlAsChild("<Item />"); if (newXmlElement == null) continue; SaveWorkspaceTreeviewItemAsXmlElement(newXmlElement, subwtvi, workspacePath); } OnWorkspaceLayoutSaved(this, new EventArgs()); } /// <summary> /// SaveWorkspaceTreeviewItemAsXmlElement 方法执行完毕。 /// </summary> public event EventHandler<EventArgs> WorkspaceLayoutSaved; protected void OnWorkspaceLayoutSaved(object sender, EventArgs e) { if (WorkspaceLayoutSaved != null) { WorkspaceLayoutSaved(sender, e); } } public bool LoadWorkspaceFromXml(string shouldSelPath) { if (Globals.MainWindow == null) return false; Globals.MainWindow.tvWorkDirectory.Items.Clear(); var xmlFilePath = WorkspaceTreeviewItemsXmlPath; if (File.Exists(xmlFilePath) == false) return false; //保证每次读取的内容都是新的。 workspaceItemsDocument = new XmlDocument(); try { workspaceItemsDocument.Load(xmlFilePath); var workspacePath = FormatPath(new FileInfo(xmlFilePath).Directory.FullName); if (workspaceItemsDocument.DocumentElement == null) return false; XmlNode node = workspaceItemsDocument.DocumentElement; var title = node.GetAttribute("Title").Value; var xmlWorkspacePath = FormatPath(node.GetAttribute("Path").Value); //由于用户可能在磁盘上移动工作区目录,故Xml文件中读取出来的Path可能是移动之前的。 bool workspaceDirectoryMoved = false; if (xmlWorkspacePath.ToLower() != workspacePath.ToLower()) { workspaceDirectoryMoved = true; } var statusHeader = node.GetAttribute("StatuHeader").Value; var statusTail = node.GetAttribute("StatuTail").Value; var isExpandAttribute = node.GetAttribute("IsExpanded"); var tooltipAttribute = node.GetAttribute("Tooltip"); string tooltip = ""; if (tooltipAttribute != null) { tooltip = tooltipAttribute.Value; } //根节点。 WorkspaceTreeViewItem wtvi = new WorkspaceTreeViewItem(title, workspacePath, statusHeader, statusTail, tooltip, WorkspaceTreeViewItem.Type.Folder, Globals.MainWindow); //IsExpanded、IsSelected在递归中设置。 Globals.MainWindow.tvWorkDirectory.Items.Add(wtvi); //递归填充下级各节点。 LoadWorkspaceTreeViewItemFromXmlNode(workspacePath, xmlWorkspacePath, workspaceDirectoryMoved, workspaceItemsDocument.DocumentElement, wtvi); var treeView = Globals.MainWindow.tvWorkDirectory; if (string.IsNullOrWhiteSpace(shouldSelPath) == false && treeView.Items.Count > 0) { var shouldSelItem = FindWorkspaceTreeViewitemByFullPath(treeView.Items[0] as WorkspaceTreeViewItem, shouldSelPath); if (shouldSelItem != null) { shouldSelItem.IsSelected = true; var parentItem = shouldSelItem.Parent as WorkspaceTreeViewItem; while (parentItem != null) { parentItem.IsExpanded = true; parentItem = parentItem.Parent as WorkspaceTreeViewItem; } } } return true; } catch (Exception ex) { MessageBox.Show(ex.Message); return false; } } /// <summary> /// 根据Xml中记录的值填充工作区管理器。 /// 要考虑用户在上次关闭工作区后手动移动工作区在磁盘上的真实物理位置的情况。 /// </summary> /// <param name="workspacePath">工作区当前真实磁盘路径。这与Xml文件根元素中记载的可能不一致。</param> /// <param name="workspaceDirectoryMoved">上次关闭工作区后,用户是否可能手动更改了工作区目录的物理磁盘路径。</param> /// <param name="node">从Xml文件中读取出来的节点(元素)。</param> /// <param name="currentItem">新实例化的工作区管理器树型节点。</param> private void LoadWorkspaceTreeViewItemFromXmlNode( string workspacePath, string xmlWorkspacePath, bool workspaceDirectoryMoved, XmlNode node, WorkspaceTreeViewItem currentItem) { if (currentItem == null || node == null) return; if (node.HasChildNodes == false) return; foreach (XmlNode childNode in node.ChildNodes) { var title = childNode.GetAttribute("Title").Value; //用户在上次关闭工作区后可能会手工移动工作区目录在磁盘上的物理路径。 //所以从Xml文件中读取出来的路径可能是错误的。 var path = childNode.GetAttribute("Path").Value; if (workspaceDirectoryMoved && path.ToLower().StartsWith(xmlWorkspacePath.ToLower())) { path = workspacePath + path.Substring(xmlWorkspacePath.Length); } else { Regex reg = new Regex(@"^[a-zA-Z]:\\.*"); var match = reg.Match(path); if (match.Success == false) { path = workspacePath + path; } } var isExpandAttribute = node.GetAttribute("IsExpanded"); var isExpanded = true; //这个默认为真 if (isExpandAttribute != null && isExpandAttribute.Value == "false") isExpanded = false; var isSelected = false; //这个默认为假 var isSelectedAttribute = node.GetAttribute("IsSelected"); if (isSelectedAttribute != null) { bool bs; if (bool.TryParse(isSelectedAttribute.Value, out bs)) { isSelected = bs; } } //注意:这里是设置当前项而不是子项。 currentItem.IsExpanded = isExpanded; currentItem.IsSelected = isSelected; var statusHeader = childNode.GetAttribute("StatuHeader").Value; var statusTail = childNode.GetAttribute("StatuTail").Value; var tooltip = ""; var tooltipAttribute = childNode.GetAttribute("Tooltip"); if (tooltipAttribute != null) { tooltip = tooltipAttribute.Value; } var itemTypeText = childNode.GetAttribute("ItemType").Value; WorkspaceTreeViewItem.Type itemType; if (string.IsNullOrEmpty(itemTypeText)) { itemType = WorkspaceTreeViewItem.Type.File; } else if (Enum.TryParse<WorkspaceTreeViewItem.Type>(itemTypeText, out itemType) == false) { itemType = WorkspaceTreeViewItem.Type.File; } var newItem = new WorkspaceTreeViewItem(title, path, statusHeader, statusTail, tooltip, itemType, Globals.MainWindow); currentItem.Items.Add(newItem); LoadWorkspaceTreeViewItemFromXmlNode(workspacePath, xmlWorkspacePath, workspaceDirectoryMoved, childNode, newItem); } } /// <summary> /// 刷新工作区管理器。 /// </summary> /// <param name="shouldSelPath">刷新后寻找指向这个路径的条目并选中该条目。</param> public void RefreshWorkspaceTreeView(string shouldSelPath) { if (this.masterWindow == null) return; //if (LoadWorkspaceFromXml(shouldSelPath)) return; //只有在载入工作区时才应使用这个方法。否则可能会出错。因为这个Xml文件只会在关闭工作区(或主窗口)时才会更新。 TreeView treeView = this.masterWindow.tvWorkDirectory; treeView.Items.Clear();//全部清除。 if (Directory.Exists(Globals.PathOfWorkspace) == false) { return; } WorkspaceTreeViewItem rootItem = new WorkspaceTreeViewItem(Globals.PathOfWorkspace, this.masterWindow) { IsExpanded = true, ItemType = WorkspaceTreeViewItem.Type.Folder, }; DirectoryInfo directoryInfo = new DirectoryInfo(Globals.PathOfWorkspace); FillWorkspaceTreeViewItem(directoryInfo, rootItem); if (string.IsNullOrWhiteSpace(shouldSelPath) == false && treeView.Items.Count > 0) { var shouldSelItem = FindWorkspaceTreeViewitemByFullPath(treeView.Items[0] as WorkspaceTreeViewItem, shouldSelPath); if (shouldSelItem == null) return; shouldSelItem.IsSelected = true; var parentItem = shouldSelItem.Parent as WorkspaceTreeViewItem; while (parentItem != null) { parentItem.IsExpanded = true; parentItem = parentItem.Parent as WorkspaceTreeViewItem; } } treeView.Items.Add(rootItem); } /// <summary> /// [递归方法]在工作区管理器中寻找指向指定路径的条目。 /// </summary> /// <param name="item">从哪个条目开始找。</param> /// <param name="fullPath">要查找的路径。</param> private WorkspaceTreeViewItem FindWorkspaceTreeViewitemByFullPath(WorkspaceTreeViewItem item, string fullPath) { if (item == null) return null; if (string.IsNullOrWhiteSpace(fullPath)) return null; if (item.FullPath == fullPath) return item; foreach (WorkspaceTreeViewItem wi in item.Items) { if (wi != null && wi.FullPath == fullPath) return wi; if (wi.HasItems) { var subItem = FindWorkspaceTreeViewitemByFullPath(wi, fullPath); if (subItem != null) return subItem; } } return null; } /// <summary> /// [递归方法],根据当前目录路径填空相关条目到此目录对应的工作区条目中。 /// </summary> /// <param name="directoryInfo">要生成条目的目录。</param> /// <param name="curItem">将生成的新条目添加到哪个条目作为子级。</param> void FillWorkspaceTreeViewItem(System.IO.DirectoryInfo curDirectoryInfo, WorkspaceTreeViewItem curItem) { if (curDirectoryInfo == null || curItem == null) return; List<WorkspaceTreeViewItem> tempItems = new List<WorkspaceTreeViewItem>(); DirectoryInfo[] childDirectoryInfos = curDirectoryInfo.GetDirectories(); if (childDirectoryInfos.Length > 0) { foreach (DirectoryInfo childDirectoryInfo in childDirectoryInfos) { if (childDirectoryInfo.FullName.EndsWith("~")) continue;//以波形符结尾的目录是对应Markdown文件的图像存储目录。 bool isResource = (childDirectoryInfo.Name.EndsWith("~")); WorkspaceTreeViewItem newDirectoryItem = new WorkspaceTreeViewItem(childDirectoryInfo.FullName, this.masterWindow) { IsExpanded = !isResource, ItemType = WorkspaceTreeViewItem.Type.Folder, }; tempItems.Add(newDirectoryItem); //添加目录元文件相关资源文件 var metaDirectoryFileBaseName = (childDirectoryInfo.FullName.EndsWith("\\") ? childDirectoryInfo.FullName : childDirectoryInfo.FullName + "\\") + "_" + childDirectoryInfo.Name; var metaDirectoryFile = metaDirectoryFileBaseName + ".md"; //if (File.Exists(metaDirectoryFile)) //{//去除此限制,只要存在资源文件就显示 var metaResourceDirectory = metaDirectoryFileBaseName + "~"; if (Directory.Exists(metaResourceDirectory)) { DirectoryInfo resourceDirectoryInfo = new DirectoryInfo(metaResourceDirectory); DirectoryInfo[] childResourceDirectoryInfos = resourceDirectoryInfo.GetDirectories(); foreach (var childResourceDirectoryInfo in childResourceDirectoryInfos) { //不显示资源文件夹本身,只显示资源文件夹下的子文件夹(例如:Images~) if (childResourceDirectoryInfo.FullName.EndsWith("~") || childResourceDirectoryInfo.FullName.EndsWith("~\\")) { WorkspaceTreeViewItem newResourceSubDirectoryItem = new WorkspaceTreeViewItem(childResourceDirectoryInfo.FullName, this.masterWindow) { IsExpanded = false, }; if (childResourceDirectoryInfo.Name.StartsWith("Images~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.ImageFolder; } else if (childResourceDirectoryInfo.Name.StartsWith("Sounds~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.SoundFolder; } else if (childResourceDirectoryInfo.Name.StartsWith("Vedios~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.VedioFolder; } newDirectoryItem.Items.Add(newResourceSubDirectoryItem); var childResourceFileInfos = childResourceDirectoryInfo.GetFiles(); foreach (var childResourceFileInfo in childResourceFileInfos) { if (childResourceFileInfo.Name == "~.txt") continue; WorkspaceTreeViewItem newResourceFileItem = new WorkspaceTreeViewItem(childResourceFileInfo.FullName, this.masterWindow); switch (newResourceSubDirectoryItem.ItemType) { case WorkspaceTreeViewItem.Type.ImageFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Image; break; case WorkspaceTreeViewItem.Type.SoundFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Sound; break; case WorkspaceTreeViewItem.Type.VedioFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Vedio; break; } newResourceSubDirectoryItem.Items.Add(newResourceFileItem); } } } } //} FillWorkspaceTreeViewItem(childDirectoryInfo, newDirectoryItem); } } FileInfo[] childFileInfos = curDirectoryInfo.GetFiles(); if (childFileInfos.Length > 0) { //再添加普通md文件。 foreach (FileInfo childFileInfo in childFileInfos) { if (childFileInfo.Extension.ToLower() == ".md" && childFileInfo.FullName.ToLower().EndsWith("_tmp~.md") == false) { if (childFileInfo.Name.ToLower() == ("_" + curDirectoryInfo.Name + ".md").ToLower()) continue;//已添加目录对应的特殊文件,此文件会被当作目录指向的文件进行处理。 WorkspaceTreeViewItem newFileItem = new WorkspaceTreeViewItem(childFileInfo.FullName, this.masterWindow) { IsExpanded = true, }; tempItems.Add(newFileItem); //寻找此Md文件对应的资源文件夹。名称应为:文件名(去后缀名)+波形符 var resourceDirectory = childFileInfo.FullName.Substring(0, childFileInfo.FullName.Length - 3) + "~"; if (Directory.Exists(resourceDirectory)) { DirectoryInfo resourceDirectoryInfo = new DirectoryInfo(resourceDirectory); DirectoryInfo[] childResourceDirectoryInfos = resourceDirectoryInfo.GetDirectories(); foreach (var childResourceDirectoryInfo in childResourceDirectoryInfos) { //不显示资源文件夹本身,只显示资源文件夹下的子文件夹(例如:Images~) if (childResourceDirectoryInfo.FullName.EndsWith("~") || childResourceDirectoryInfo.FullName.EndsWith("~\\")) { WorkspaceTreeViewItem newResourceSubDirectoryItem = new WorkspaceTreeViewItem(childResourceDirectoryInfo.FullName, this.masterWindow) { IsExpanded = false, }; if (childResourceDirectoryInfo.Name.StartsWith("Images~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.ImageFolder; } else if (childResourceDirectoryInfo.Name.StartsWith("Sounds~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.SoundFolder; } else if (childResourceDirectoryInfo.Name.StartsWith("Vedios~")) { newResourceSubDirectoryItem.ItemType = WorkspaceTreeViewItem.Type.VedioFolder; } newFileItem.Items.Add(newResourceSubDirectoryItem); var childResourceFileInfos = childResourceDirectoryInfo.GetFiles(); foreach (var childResourceFileInfo in childResourceFileInfos) { if (childResourceFileInfo.Name == "~.txt") continue; WorkspaceTreeViewItem newResourceFileItem = new WorkspaceTreeViewItem(childResourceFileInfo.FullName, this.masterWindow); switch (newResourceSubDirectoryItem.ItemType) { case WorkspaceTreeViewItem.Type.ImageFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Image; break; case WorkspaceTreeViewItem.Type.SoundFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Sound; break; case WorkspaceTreeViewItem.Type.VedioFolder: newResourceFileItem.ItemType = WorkspaceTreeViewItem.Type.Vedio; break; } newResourceSubDirectoryItem.Items.Add(newResourceFileItem); } } } } } else if (IsImageFile(childFileInfo.FullName)) { //如果是图像文件,也不显示,其它文件不显示 WorkspaceTreeViewItem newFileItem = new WorkspaceTreeViewItem(childFileInfo.FullName, this.masterWindow) { IsExpanded = false, }; tempItems.Add(newFileItem); } } } if (tempItems.Count > 0) { List<WorkspaceTreeViewItem> tmp2Items = new List<WorkspaceTreeViewItem>(); List<WorkspaceTreeViewItem> tmp3Items = new List<WorkspaceTreeViewItem>(); foreach (var i in tempItems) { if (i.ShortName.StartsWith("_")) { tmp2Items.Add(i); } else tmp3Items.Add(i); } tmp2Items.Sort(new WorkspaceTreeViewItemCompare()); tmp3Items.Sort(new WorkspaceTreeViewItemCompare()); foreach (var i in tmp2Items) { curItem.Items.Add(i); } foreach (var i in tmp3Items) { curItem.Items.Add(i); } } } /// <summary> /// 检验指定目录是否有效的图像文件名。 /// </summary> /// <param name="fullName"></param> /// <returns></returns> public static bool IsImageFile(string fullName) { if (string.IsNullOrEmpty(fullName)) return false; if (File.Exists(fullName) == false) return false; var lower = fullName.ToLower(); if (lower.EndsWith(".jpg") || lower.EndsWith(".jpeg") || lower.EndsWith(".gif") || lower.EndsWith(".png") || lower.EndsWith(".bmp") || lower.EndsWith(".ico") || lower.EndsWith(".tiff")) return true; return false; } } }