#region License information
// ----------------------------------------------------------------------------
//
// Eq2VpkTool - A tool to extract Everquest II VPK files
// Blaz (blaz@blazlabs.com)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
//
// ( The full text of the license can be found in the License.txt file )
//
// ----------------------------------------------------------------------------
#endregion
#region Using directives
using System;
using System.Threading;
using System.Windows.Forms;
using System.Collections.Generic;
using System.IO;
using Eq2FileSystem = Everquest2.IO.FileSystem;
using Eq2FileSystemInfo = Everquest2.IO.FileSystemInfo;
using Eq2FileInfo = Everquest2.IO.FileInfo;
using Eq2DirectoryInfo = Everquest2.IO.DirectoryInfo;
#endregion
namespace Eq2VpkTool
{
public class FileSystemViewController
{
#region Methods
public void Open(string filename, TreeView treeview, ListView listview)
{
this.treeview = treeview;
this.listview = listview;
nodeDictionary.Clear();
fileCount = 0;
filesystem = new Eq2FileSystem();
extractionManager = new ExtractionManager();
// Initialize treeview and listview
treeview.Nodes.Clear();
listview.Items.Clear();
listview.Sorting = SortOrder.Ascending;
listview.ListViewItemSorter = new DirectoryContentsComparer();
iconManager = new IconManager(new ImageList());
treeview.ImageList = iconManager.ImageList;
treeview.ImageIndex = iconManager.GetDirectoryImageIndex();
listview.SmallImageList = iconManager.ImageList;
// We must first de-register the event handlers, in case they were registered in a previous call to Open().
treeview.BeforeExpand -= OnExpandNode;
treeview.BeforeExpand += OnExpandNode;
treeview.BeforeCollapse -= OnCollapseNode;
treeview.BeforeCollapse += OnCollapseNode;
treeview.BeforeSelect -= OnSelectNode;
treeview.BeforeSelect += OnSelectNode;
listview.DoubleClick -= OnListViewDoubleClick;
listview.DoubleClick += OnListViewDoubleClick;
thread = new Thread(new ParameterizedThreadStart(OpenFileSystem));
thread.Start(filename);
}
///
/// Start function for the processing thread.
///
/// String that represents the path to the VPL file.
private void OpenFileSystem(object obj)
{
filename = obj as string;
try
{
filesystem.DirectoryAdded += OnRootDirectoryAdded;
filesystem.FileAdded += OnFileAdded;
filesystem.Open(filename);
}
catch (ThreadAbortException)
{
}
catch (Exception e)
{
MessageBox.Show("Error processing file.\n\n" + e, "Error");
}
}
///
/// Stops the processing thread and deletes all generated temporary files.
///
public void Close()
{
if (thread != null)
{
thread.Abort();
thread.Join();
thread = null;
// Delete temporary files
foreach (string path in temporaryFiles)
{
try { File.Delete(path); }
catch (UnauthorizedAccessException) {}
catch (IOException) {}
}
}
}
///
/// Callback invoked every time a file is added to the file system.
///
///
///
private void OnFileAdded(object sender, Eq2FileSystem.FileAddedEventArgs e)
{
lock (this) ++fileCount;
}
///
/// Callback invoked when the first directory (the root directory) is added to the file system.
///
///
///
private void OnRootDirectoryAdded(object sender, Eq2FileSystem.DirectoryAddedEventArgs e)
{
filesystem.DirectoryAdded -= OnRootDirectoryAdded;
e.directory.DirectoryAdded += OnDirectoryAdded;
treeview.Invoke(new Action(AddRootDirectory), e.directory);
}
///
/// Adds the root directory node to the TreeView.
///
/// This method is called from the UI thread.
/// Root directory.
private void AddRootDirectory(Eq2DirectoryInfo rootDirectory)
{
string rootNodeText = System.IO.Path.GetFileName(filename);
TreeNode node = new TreeNode(rootNodeText);
node.Tag = rootDirectory;
treeview.Nodes.Add(node);
// No need to acquire the lock on nodeDictionary because we're sure
// no other thread will be accessing it at this point.
nodeDictionary.Add(rootDirectory, new NodeInfo(node));
}
///
/// Callback invoked every time a new directory is added to an existing directory.
///
///
///
private void OnDirectoryAdded(object sender, Eq2FileSystem.DirectoryAddedEventArgs e)
{
Eq2DirectoryInfo directory = e.directory;
Eq2DirectoryInfo parentDirectory = directory.Parent;
NodeInfo parentDirectoryNodeInfo;
lock (nodeDictionary) parentDirectoryNodeInfo = nodeDictionary[parentDirectory];
if (parentDirectoryNodeInfo.Expanded)
{
// The parent directory is expanded, so we must reflect the addition of the new directory.
// This new directory doesn't have any subdirectories yet, so we will install a listener on it.
TreeNode directoryNode = treeview.Invoke(new AddDirectoryDelegate(AddDirectoryToExpandedDirectory),
parentDirectoryNodeInfo.Node,
directory) as TreeNode;
lock (nodeDictionary) nodeDictionary.Add(directory, new NodeInfo(directoryNode));
directory.DirectoryAdded += OnDirectoryAdded;
}
else
{
// The parent directory didn't have any subdirectories, but it now has one.
// We'll add a dummy child node so the plus icon appears and the node can be expanded.
// Note: More than one thread can invoke this method before we deregister from the DirectoryAdded event.
// The means more than one dummy subnode can be added to this directory.
parentDirectory.DirectoryAdded -= OnDirectoryAdded;
treeview.Invoke(new Action(AddDirectoryToCollapsedDirectory), parentDirectoryNodeInfo.Node);
}
}
///
/// Adds a new tree node as a child of an expanded directory node.
///
/// This method is called from the UI thread.
///
///
///
private TreeNode AddDirectoryToExpandedDirectory(TreeNode parentNode, Eq2DirectoryInfo directory)
{
string directoryName = directory.Name;
TreeNode node = parentNode.Nodes.Add(directoryName, directoryName);
node.Tag = directory;
return node;
}
///
/// Adds a dummy tree node as a child of a collapsed directory node.
/// This dummy node makes the treeview render a plus sign used to expand the directory node.
///
/// This method is called from the UI thread.
///
private void AddDirectoryToCollapsedDirectory(TreeNode directoryNode)
{
directoryNode.Nodes.Add(new TreeNode());
}
///
/// Callback invoked when a tree node is expanded.
/// This method must create the nodes for the subdirectories of the expanded directory.
///
/// This method is called from the UI thread.
///
///
private void OnExpandNode(object sender, TreeViewCancelEventArgs e)
{
TreeNode node = e.Node;
Eq2DirectoryInfo directory = node.Tag as Eq2DirectoryInfo;
// Remove dummy nodes if present
if (node.Nodes[0].Text.Length == 0) node.Nodes.Clear();
lock (nodeDictionary)
{
treeview.BeginUpdate();
// Add new subdirectories as nodes to the treeview and to the node dictionary.
Eq2DirectoryInfo[] subdirectories = directory.GetDirectories();
foreach (Eq2DirectoryInfo subdirectory in subdirectories)
{
if (!node.Nodes.ContainsKey(subdirectory.Name))
{
TreeNode subdirectoryNode = AddDirectoryToExpandedDirectory(node, subdirectory);
nodeDictionary.Add(subdirectory, new NodeInfo(subdirectoryNode));
// Check whether this subdirectory has subdirectories of its own or not.
// If it does, we will add a dummy tree node to it so the plus icon appears.
// If it does not, we will track any changes to it.
if (subdirectory.DirectoryCount > 0)
{
subdirectoryNode.Nodes.Add(new TreeNode());
}
else
{
subdirectory.DirectoryAdded += OnDirectoryAdded;
}
}
}
treeview.EndUpdate();
// Mark this directory as expanded
nodeDictionary[directory].Expanded = true;
directory.DirectoryAdded += OnDirectoryAdded;
}
}
///
/// Callback invoked when a tree node is collapsed.
/// We won't delete the tree nodes already added to the collapsed directory,
/// but we won't add any more until the node is expanded again.
///
/// This method is called from the UI thread.
///
///
private void OnCollapseNode(object sender, TreeViewCancelEventArgs e)
{
TreeNode node = e.Node;
Eq2DirectoryInfo directory = node.Tag as Eq2DirectoryInfo;
directory.DirectoryAdded -= OnDirectoryAdded;
// Mark this directory as collapsed
lock (nodeDictionary) nodeDictionary[directory].Expanded = false;
}
///
/// Callback invoked when a tree node is selected.
/// Updates the list view with the directory contents.
///
/// This method is called from the UI thread.
///
///
private void OnSelectNode(object sender, TreeViewCancelEventArgs e)
{
TreeView treeview = sender as TreeView;
TreeNode node = e.Node;
Eq2DirectoryInfo newDirectory = node.Tag as Eq2DirectoryInfo;
if (treeview.SelectedNode != null)
{
// Deregister from the old directory
Eq2DirectoryInfo oldDirectory = treeview.SelectedNode.Tag as Eq2DirectoryInfo;
oldDirectory.ChildAdded -= OnDirectoryChildAdded;
}
// FIXME: There is a race condition here. It is possible that after having deregistered from
// the old directory there is still a child of that directory in the process of being
// added. In that case, it will be added to the listview erroneously as if it were a
// child of the newly selected directory.
// Clear the old items from the listview
listview.Items.Clear();
listview.BeginUpdate();
// Add the current children of the new directory to the listview
Eq2FileSystemInfo[] children = newDirectory.GetFileSystemInfos();
foreach (Eq2FileSystemInfo child in children)
{
ListViewItem item = listview.Items.Add(child.Name);
item.Tag = child;
item.ImageIndex = iconManager.GetImageIndex(child);
}
listview.EndUpdate();
// Register to receive child added events in the future to the new directory
newDirectory.ChildAdded += OnDirectoryChildAdded;
}
///
/// Callback invoked when a file or directory is added to an existing directory.
///
///
///
private void OnDirectoryChildAdded(object sender, Eq2DirectoryInfo.ChildAddedEventArgs e)
{
listview.Invoke(new AddChildToDirectoryDelegate(AddChildToDirectory), sender as Eq2DirectoryInfo, e.child);
}
///
/// Adds a file or directory to the list view.
///
/// This method is called from the UI thread.
///
///
private void AddChildToDirectory(Eq2DirectoryInfo directory, Eq2FileSystemInfo child)
{
// Note: The second condition is probably not needed.
if (treeview.SelectedNode != null && treeview.SelectedNode.Tag == directory)
{
ListViewItem item = listview.Items.Add(child.Name);
item.Tag = child;
item.ImageIndex = iconManager.GetImageIndex(child);
}
}
///
/// Callback invoked when the user double-clicks on the list view.
/// This operation opens the selected directory or extracts and opens the selected file.
///
/// This method is called from the UI thread.
///
///
private void OnListViewDoubleClick(object sender, EventArgs e)
{
ListView listview = sender as ListView;
if (listview.SelectedItems.Count != 1) return;
Eq2FileSystemInfo child = listview.SelectedItems[0].Tag as Eq2FileSystemInfo;
if (child is Eq2DirectoryInfo)
{
OnListViewDoubleClickDirectory(child as Eq2DirectoryInfo);
}
else if (child is Eq2FileInfo)
{
OnListViewDoubleClickFile(child as Eq2FileInfo);
}
}
private void OnListViewDoubleClickDirectory(Eq2DirectoryInfo directory)
{
Stack directoryHierarchy = new Stack();
directoryHierarchy.Push(directory);
Eq2DirectoryInfo parent = directory.Parent;
while (parent != null)
{
directoryHierarchy.Push(parent);
parent = parent.Parent;
}
// Pop root directory
directoryHierarchy.Pop();
TreeNode rootNode = treeview.Nodes[0];
rootNode.Expand();
TreeNodeCollection directoryNodes = rootNode.Nodes;
TreeNode nodeToExpand = null;
while (directoryHierarchy.Count > 0)
{
Eq2DirectoryInfo directoryToExpand = directoryHierarchy.Pop();
nodeToExpand = directoryNodes[directoryToExpand.Name];
if (!nodeToExpand.IsExpanded) nodeToExpand.Expand();
directoryNodes = nodeToExpand.Nodes;
}
treeview.SelectedNode = nodeToExpand;
}
private void OnListViewDoubleClickFile(Eq2FileInfo file)
{
OpenFile(file);
}
private void OpenFile(Eq2FileInfo file)
{
string path = Path.GetTempPath() + file.DirectoryName.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
string filename = path + file.Name;
try
{
Directory.CreateDirectory(path);
FileStream stream = extractionManager.ExtractFile(file, path);
stream.Close();
}
catch (UnauthorizedAccessException) { return; }
catch (IOException) { return; }
try
{
temporaryFiles.Add(filename);
System.Diagnostics.Process.Start(filename);
}
catch (System.ComponentModel.Win32Exception)
{
// There is no associated program for this file type.
// Ignore the error.
}
}
#endregion
#region Types
///
/// Contains information about a tree view node. This information is used by the processing thread,
/// not the UI thread. It's a way of reducing the number of cross-thread invocations.
///
private class NodeInfo
{
public NodeInfo(TreeNode node)
{
this.node = node;
this.expanded = false;
}
public TreeNode Node
{
get { return node; }
}
public bool Expanded
{
get { return expanded; }
set { expanded = value; }
}
private TreeNode node;
private bool expanded;
}
#endregion
#region Delegates
private delegate TreeNode AddDirectoryDelegate (TreeNode parentNode, Eq2DirectoryInfo directory);
private delegate void AddChildToDirectoryDelegate (Eq2DirectoryInfo directory, Eq2FileSystemInfo child);
#endregion
#region Properties
public int FileCount
{
get { lock (this) return fileCount; }
}
public Eq2FileSystem FileSystem
{
get { return filesystem; }
}
#endregion
#region Fields
private IDictionary nodeDictionary = new Dictionary();
private IList temporaryFiles = new List();
private int fileCount;
private string filename;
private Thread thread;
private Eq2FileSystem filesystem;
private TreeView treeview;
private ListView listview;
private IconManager iconManager;
private ExtractionManager extractionManager;
#endregion
}
}