From b24b13aa16b7ee1720bdbba44c9346608811b79a Mon Sep 17 00:00:00 2001 From: Aleks Margarian Date: Sun, 28 Apr 2019 20:46:15 -0700 Subject: [PATCH] First Commit --- ...AurelienRibon.Ui.SyntaxHighlightBox.csproj | 81 +++ .../Properties/AssemblyInfo.cs | 53 ++ .../resources/JSON.xml | 27 + .../resources/syntax.xsd | 52 ++ .../src/DrawingControl.cs | 33 ++ .../src/HighlighterManager.cs | 221 +++++++++ .../src/IHighlighter.cs | 18 + .../src/SyntaxHighlightBox.xaml | 58 +++ .../src/SyntaxHighlightBox.xaml.cs | 401 +++++++++++++++ .../src/TextUtilities.cs | 66 +++ Silver.sln | 31 ++ Silver/App.config | 6 + Silver/App.xaml | 9 + Silver/App.xaml.cs | 17 + Silver/FSCollection.cs | 145 ++++++ Silver/FileViewer.xaml | 57 +++ Silver/FileViewer.xaml.cs | 144 ++++++ Silver/FodyWeavers.xml | 4 + Silver/FodyWeavers.xsd | 111 +++++ Silver/Helpers.cs | 160 ++++++ Silver/MainWindow.xaml | 105 ++++ Silver/MainWindow.xaml.cs | 460 ++++++++++++++++++ Silver/PanelItem.cs | 66 +++ Silver/PathHistory.cs | 58 +++ Silver/Project.cs | 250 ++++++++++ Silver/ProjectProps.xaml | 34 ++ Silver/ProjectProps.xaml.cs | 107 ++++ Silver/Properties/AssemblyInfo.cs | 53 ++ Silver/Properties/Resources.Designer.cs | 63 +++ Silver/Properties/Resources.resx | 120 +++++ Silver/Properties/Settings.Designer.cs | 30 ++ Silver/Properties/Settings.settings | 7 + Silver/Resources/favicon.ico | Bin 0 -> 159417 bytes Silver/Resources/file.ico | Bin 0 -> 46473 bytes Silver/Resources/folder.ico | Bin 0 -> 51902 bytes Silver/Resources/json.png | Bin 0 -> 20171 bytes Silver/Resources/up.ico | Bin 0 -> 69998 bytes Silver/Resources/upDisabled.ico | Bin 0 -> 69998 bytes Silver/Silver.csproj | 175 +++++++ Silver/packages.config | 8 + 40 files changed, 3230 insertions(+) create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/AurelienRibon.Ui.SyntaxHighlightBox.csproj create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/Properties/AssemblyInfo.cs create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/resources/JSON.xml create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/resources/syntax.xsd create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/DrawingControl.cs create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/HighlighterManager.cs create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/IHighlighter.cs create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml.cs create mode 100644 AurelienRibon.Ui.SyntaxHighlightBox/src/TextUtilities.cs create mode 100644 Silver.sln create mode 100644 Silver/App.config create mode 100644 Silver/App.xaml create mode 100644 Silver/App.xaml.cs create mode 100644 Silver/FSCollection.cs create mode 100644 Silver/FileViewer.xaml create mode 100644 Silver/FileViewer.xaml.cs create mode 100644 Silver/FodyWeavers.xml create mode 100644 Silver/FodyWeavers.xsd create mode 100644 Silver/Helpers.cs create mode 100644 Silver/MainWindow.xaml create mode 100644 Silver/MainWindow.xaml.cs create mode 100644 Silver/PanelItem.cs create mode 100644 Silver/PathHistory.cs create mode 100644 Silver/Project.cs create mode 100644 Silver/ProjectProps.xaml create mode 100644 Silver/ProjectProps.xaml.cs create mode 100644 Silver/Properties/AssemblyInfo.cs create mode 100644 Silver/Properties/Resources.Designer.cs create mode 100644 Silver/Properties/Resources.resx create mode 100644 Silver/Properties/Settings.Designer.cs create mode 100644 Silver/Properties/Settings.settings create mode 100644 Silver/Resources/favicon.ico create mode 100644 Silver/Resources/file.ico create mode 100644 Silver/Resources/folder.ico create mode 100644 Silver/Resources/json.png create mode 100644 Silver/Resources/up.ico create mode 100644 Silver/Resources/upDisabled.ico create mode 100644 Silver/Silver.csproj create mode 100644 Silver/packages.config diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/AurelienRibon.Ui.SyntaxHighlightBox.csproj b/AurelienRibon.Ui.SyntaxHighlightBox/AurelienRibon.Ui.SyntaxHighlightBox.csproj new file mode 100644 index 0000000..1e34024 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/AurelienRibon.Ui.SyntaxHighlightBox.csproj @@ -0,0 +1,81 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {10A9ECD3-AE1E-494D-9A27-8A32DD581759} + Library + Properties + AurelienRibon.Ui.SyntaxHighlightBox + AurelienRibon.Ui.SyntaxHighlightBox + v4.7.2 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + SyntaxHighlightBox.xaml + + + + + + + + + MSBuild:Compile + Designer + + + + + Designer + + + + + + + + \ No newline at end of file diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/Properties/AssemblyInfo.cs b/AurelienRibon.Ui.SyntaxHighlightBox/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..759f4fe --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/Properties/AssemblyInfo.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Silver")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Silver")] +[assembly: AssemblyCopyright("Copyright © WorkingRobot 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/resources/JSON.xml b/AurelienRibon.Ui.SyntaxHighlightBox/resources/JSON.xml new file mode 100644 index 0000000..94cceaf --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/resources/JSON.xml @@ -0,0 +1,27 @@ + + + + + [{}\[\],:] + false + #0080FF + Bold + Normal + + + + \d+(?:\.\d+)? + false + #307F80 + Normal + Normal + + + + \"(?:\\.|[^"\\])*\" + false + #F68A1B + Normal + Normal + + diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/resources/syntax.xsd b/AurelienRibon.Ui.SyntaxHighlightBox/resources/syntax.xsd new file mode 100644 index 0000000..a007554 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/resources/syntax.xsd @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/DrawingControl.cs b/AurelienRibon.Ui.SyntaxHighlightBox/src/DrawingControl.cs new file mode 100644 index 0000000..2ea62a7 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/DrawingControl.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Media; + +namespace AurelienRibon.Ui.SyntaxHighlightBox { + public class DrawingControl : FrameworkElement { + private VisualCollection visuals; + private DrawingVisual visual; + + public DrawingControl() { + visual = new DrawingVisual(); + visuals = new VisualCollection(this); + visuals.Add(visual); + } + + public DrawingContext GetContext() { + return visual.RenderOpen(); + } + + protected override int VisualChildrenCount { + get { return visuals.Count; } + } + + protected override Visual GetVisualChild(int index) { + if (index < 0 || index >= visuals.Count) + throw new ArgumentOutOfRangeException(); + return visuals[index]; + } + } +} diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/HighlighterManager.cs b/AurelienRibon.Ui.SyntaxHighlightBox/src/HighlighterManager.cs new file mode 100644 index 0000000..d862a27 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/HighlighterManager.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; +using System.Windows; +using System.Xml.Linq; +using System.Xml.Schema; +using System.Diagnostics; +using System.Xml; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Collections; +using System.Text.RegularExpressions; + +namespace AurelienRibon.Ui.SyntaxHighlightBox { + public class HighlighterManager { + private static HighlighterManager instance = new HighlighterManager(); + public static HighlighterManager Instance { get { return instance; } } + + public IDictionary Highlighters { get; private set; } + + private HighlighterManager() { + Highlighters = new Dictionary(); + + var gfdgd = Application.GetResourceStream(new Uri("pack://application:,,,/AurelienRibon.Ui.SyntaxHighlightBox;component/resources/syntax.xsd")); + var schemaStream = gfdgd.Stream; + XmlSchema schema = XmlSchema.Read(schemaStream, (s, e) => { + Debug.WriteLine("Xml schema validation error : " + e.Message); + }); + + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.Schemas.Add(schema); + readerSettings.ValidationType = ValidationType.Schema; + + foreach (var res in GetResources("resources/(.+?)[.]xml")) { + XDocument xmldoc = null; + try { + XmlReader reader = XmlReader.Create(res.Value, readerSettings); + xmldoc = XDocument.Load(reader); + } catch (XmlSchemaValidationException ex) { + Debug.WriteLine("Xml validation error at line " + ex.LineNumber + " for " + res.Key + " :"); + Debug.WriteLine("Warning : if you cannot find the issue in the xml file, verify the xsd file."); + Debug.WriteLine(ex.Message); + return; + } catch (Exception ex) { + Debug.WriteLine(ex.Message); + return; + } + + XElement root = xmldoc.Root; + String name = root.Attribute("name").Value.Trim(); + Highlighters.Add(name, new XmlHighlighter(root)); + } + } + + /// + /// Returns a dictionary of the assembly resources (not embedded). + /// Uses a regex filter for the resource paths. + /// + private IDictionary GetResources(string filter) { + var asm = Assembly.GetCallingAssembly(); + string resName = asm.GetName().Name + ".g.resources"; + Stream manifestStream = asm.GetManifestResourceStream(resName); + ResourceReader reader = new ResourceReader(manifestStream); + + IDictionary ret = new Dictionary(); + foreach (DictionaryEntry res in reader) { + string path = (string)res.Key; + UnmanagedMemoryStream stream = (UnmanagedMemoryStream)res.Value; + if (Regex.IsMatch(path, filter)) + ret.Add(path, stream); + } + return ret; + } + + /// + /// An IHighlighter built from an Xml syntax file + /// + private class XmlHighlighter : IHighlighter { + private List wordsRules; + private List lineRules; + private List regexRules; + + public XmlHighlighter(XElement root) { + wordsRules = new List(); + lineRules = new List(); + regexRules = new List(); + + foreach (XElement elem in root.Elements()) { + switch (elem.Name.ToString()) { + case "HighlightWordsRule": wordsRules.Add(new HighlightWordsRule(elem)); break; + case "HighlightLineRule": lineRules.Add(new HighlightLineRule(elem)); break; + case "AdvancedHighlightRule": regexRules.Add(new AdvancedHighlightRule(elem)); break; + } + } + } + + public int Highlight(FormattedText text, int previousBlockCode) { + // + // WORDS RULES + // + Regex wordsRgx = new Regex("[a-zA-Z_][a-zA-Z0-9_]*"); + foreach (Match m in wordsRgx.Matches(text.Text)) { + foreach (HighlightWordsRule rule in wordsRules) { + foreach (string word in rule.Words) { + if (rule.Options.IgnoreCase) { + if (m.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase)) { + text.SetForegroundBrush(rule.Options.Foreground, m.Index, m.Length); + text.SetFontWeight(rule.Options.FontWeight, m.Index, m.Length); + text.SetFontStyle(rule.Options.FontStyle, m.Index, m.Length); + } + } else { + if (m.Value == word) { + text.SetForegroundBrush(rule.Options.Foreground, m.Index, m.Length); + text.SetFontWeight(rule.Options.FontWeight, m.Index, m.Length); + text.SetFontStyle(rule.Options.FontStyle, m.Index, m.Length); + } + } + } + } + } + + // + // REGEX RULES + // + foreach (AdvancedHighlightRule rule in regexRules) { + Regex regexRgx = new Regex(rule.Expression); + foreach (Match m in regexRgx.Matches(text.Text)) { + text.SetForegroundBrush(rule.Options.Foreground, m.Index, m.Length); + text.SetFontWeight(rule.Options.FontWeight, m.Index, m.Length); + text.SetFontStyle(rule.Options.FontStyle, m.Index, m.Length); + } + } + + // + // LINES RULES + // + foreach (HighlightLineRule rule in lineRules) { + Regex lineRgx = new Regex(Regex.Escape(rule.LineStart) + ".*"); + foreach (Match m in lineRgx.Matches(text.Text)) { + text.SetForegroundBrush(rule.Options.Foreground, m.Index, m.Length); + text.SetFontWeight(rule.Options.FontWeight, m.Index, m.Length); + text.SetFontStyle(rule.Options.FontStyle, m.Index, m.Length); + } + } + + return -1; + } + } + + /// + /// A set of words and their RuleOptions. + /// + private class HighlightWordsRule { + public List Words { get; private set; } + public RuleOptions Options { get; private set; } + + public HighlightWordsRule(XElement rule) { + Words = new List(); + Options = new RuleOptions(rule); + + string wordsStr = rule.Element("Words").Value; + string[] words = Regex.Split(wordsStr, "\\s+"); + + foreach (string word in words) + if (!string.IsNullOrWhiteSpace(word)) + Words.Add(word.Trim()); + } + } + + /// + /// A line start definition and its RuleOptions. + /// + private class HighlightLineRule { + public string LineStart { get; private set; } + public RuleOptions Options { get; private set; } + + public HighlightLineRule(XElement rule) { + LineStart = rule.Element("LineStart").Value.Trim(); + Options = new RuleOptions(rule); + } + } + + /// + /// A regex and its RuleOptions. + /// + private class AdvancedHighlightRule { + public string Expression { get; private set; } + public RuleOptions Options { get; private set; } + + public AdvancedHighlightRule(XElement rule) { + Expression = rule.Element("Expression").Value.Trim(); + Options = new RuleOptions(rule); + } + } + + /// + /// A set of options liked to each rule. + /// + private class RuleOptions { + public bool IgnoreCase { get; private set; } + public Brush Foreground { get; private set; } + public FontWeight FontWeight { get; private set; } + public FontStyle FontStyle { get; private set; } + + public RuleOptions(XElement rule) { + string ignoreCaseStr = rule.Element("IgnoreCase").Value.Trim(); + string foregroundStr = rule.Element("Foreground").Value.Trim(); + string fontWeightStr = rule.Element("FontWeight").Value.Trim(); + string fontStyleStr = rule.Element("FontStyle").Value.Trim(); + + IgnoreCase = bool.Parse(ignoreCaseStr); + Foreground = (Brush)new BrushConverter().ConvertFrom(foregroundStr); + FontWeight = (FontWeight)new FontWeightConverter().ConvertFrom(fontWeightStr); + FontStyle = (FontStyle)new FontStyleConverter().ConvertFrom(fontStyleStr); + } + } + } +} diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/IHighlighter.cs b/AurelienRibon.Ui.SyntaxHighlightBox/src/IHighlighter.cs new file mode 100644 index 0000000..2afb1fa --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/IHighlighter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; + +namespace AurelienRibon.Ui.SyntaxHighlightBox { + public interface IHighlighter { + /// + /// Highlights the text of the current block. + /// + /// The text from the current block to highlight + /// The code assigned to the previous block, or -1 if + /// there is no previous block + /// The current block code + int Highlight(FormattedText text, int previousBlockCode); + } +} diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml b/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml new file mode 100644 index 0000000..ebee268 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml.cs b/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml.cs new file mode 100644 index 0000000..7318053 --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/SyntaxHighlightBox.xaml.cs @@ -0,0 +1,401 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Windows.Input; +using System.Collections.Generic; +using System.Threading; +using System.Globalization; + +namespace AurelienRibon.Ui.SyntaxHighlightBox { + public partial class SyntaxHighlightBox : TextBox { + + // -------------------------------------------------------------------- + // Attributes + // -------------------------------------------------------------------- + + public double LineHeight { + get { return lineHeight; } + set { + if (value != lineHeight) { + lineHeight = value; + blockHeight = MaxLineCountInBlock * value; + TextBlock.SetLineStackingStrategy(this, LineStackingStrategy.BlockLineHeight); + TextBlock.SetLineHeight(this, lineHeight); + } + } + } + + public int MaxLineCountInBlock { + get { return maxLineCountInBlock; } + set { + maxLineCountInBlock = value > 0 ? value : 0; + blockHeight = value * LineHeight; + } + } + + public IHighlighter CurrentHighlighter { get; set; } + + private DrawingControl renderCanvas; + private DrawingControl lineNumbersCanvas; + private ScrollViewer scrollViewer; + private double lineHeight; + private int totalLineCount; + private List blocks; + private double blockHeight; + private int maxLineCountInBlock; + + // -------------------------------------------------------------------- + // Ctor and event handlers + // -------------------------------------------------------------------- + + public SyntaxHighlightBox() { + InitializeComponent(); + + MaxLineCountInBlock = 100; + LineHeight = FontSize * 1.3; + totalLineCount = 1; + blocks = new List(); + + Loaded += (s, e) => { + renderCanvas = (DrawingControl)Template.FindName("PART_RenderCanvas", this); + lineNumbersCanvas = (DrawingControl)Template.FindName("PART_LineNumbersCanvas", this); + scrollViewer = (ScrollViewer)Template.FindName("PART_ContentHost", this); + + lineNumbersCanvas.Width = GetFormattedTextWidth(string.Format("{0:0000}", totalLineCount)) + 5; + + scrollViewer.ScrollChanged += OnScrollChanged; + + InvalidateBlocks(0); + InvalidateVisual(); + }; + + SizeChanged += (s, e) => { + if (e.HeightChanged == false) + return; + UpdateBlocks(); + InvalidateVisual(); + }; + + TextChanged += (s, e) => { + UpdateTotalLineCount(); + InvalidateBlocks(e.Changes.First().Offset); + InvalidateVisual(); + }; + } + + protected override void OnRender(DrawingContext drawingContext) { + DrawBlocks(); + base.OnRender(drawingContext); + } + + private void OnScrollChanged(object sender, ScrollChangedEventArgs e) { + if (e.VerticalChange != 0) + UpdateBlocks(); + InvalidateVisual(); + } + + // ----------------------------------------------------------- + // Updating & Block managing + // ----------------------------------------------------------- + + private void UpdateTotalLineCount() { + totalLineCount = TextUtilities.GetLineCount(Text); + } + + private void UpdateBlocks() { + if (blocks.Count == 0) + return; + + // While something is visible after last block... + while (!blocks.Last().IsLast && blocks.Last().Position.Y + blockHeight - VerticalOffset < ActualHeight) { + int firstLineIndex = blocks.Last().LineEndIndex + 1; + int lastLineIndex = firstLineIndex + maxLineCountInBlock - 1; + lastLineIndex = lastLineIndex <= totalLineCount - 1 ? lastLineIndex : totalLineCount - 1; + + int fisrCharIndex = blocks.Last().CharEndIndex + 1; + int lastCharIndex = TextUtilities.GetLastCharIndexFromLineIndex(Text, lastLineIndex); // to be optimized (forward search) + + if (lastCharIndex <= fisrCharIndex) { + blocks.Last().IsLast = true; + return; + } + + InnerTextBlock block = new InnerTextBlock( + fisrCharIndex, + lastCharIndex, + blocks.Last().LineEndIndex + 1, + lastLineIndex, + LineHeight); + block.RawText = block.GetSubString(Text); + block.LineNumbers = GetFormattedLineNumbers(block.LineStartIndex, block.LineEndIndex); + blocks.Add(block); + FormatBlock(block, blocks.Count > 1 ? blocks[blocks.Count - 2] : null); + } + } + + private void InvalidateBlocks(int changeOffset) { + InnerTextBlock blockChanged = null; + for (int i = 0; i < blocks.Count; i++) { + if (blocks[i].CharStartIndex <= changeOffset && changeOffset <= blocks[i].CharEndIndex + 1) { + blockChanged = blocks[i]; + break; + } + } + + if (blockChanged == null && changeOffset > 0) + blockChanged = blocks.Last(); + + int fvline = blockChanged != null ? blockChanged.LineStartIndex : 0; + int lvline = GetIndexOfLastVisibleLine(); + int fvchar = blockChanged != null ? blockChanged.CharStartIndex : 0; + int lvchar = TextUtilities.GetLastCharIndexFromLineIndex(Text, lvline); + + if (blockChanged != null) + blocks.RemoveRange(blocks.IndexOf(blockChanged), blocks.Count - blocks.IndexOf(blockChanged)); + + int localLineCount = 1; + int charStart = fvchar; + int lineStart = fvline; + for (int i = fvchar; i < Text.Length; i++) { + if (Text[i] == '\n') { + localLineCount += 1; + } + if (i == Text.Length - 1) { + string blockText = Text.Substring(charStart); + InnerTextBlock block = new InnerTextBlock( + charStart, + i, lineStart, + lineStart + TextUtilities.GetLineCount(blockText) - 1, + LineHeight); + block.RawText = block.GetSubString(Text); + block.LineNumbers = GetFormattedLineNumbers(block.LineStartIndex, block.LineEndIndex); + block.IsLast = true; + + foreach (InnerTextBlock b in blocks) + if (b.LineStartIndex == block.LineStartIndex) + throw new Exception(); + + blocks.Add(block); + FormatBlock(block, blocks.Count > 1 ? blocks[blocks.Count - 2] : null); + break; + } + if (localLineCount > maxLineCountInBlock) { + InnerTextBlock block = new InnerTextBlock( + charStart, + i, + lineStart, + lineStart + maxLineCountInBlock - 1, + LineHeight); + block.RawText = block.GetSubString(Text); + block.LineNumbers = GetFormattedLineNumbers(block.LineStartIndex, block.LineEndIndex); + + foreach (InnerTextBlock b in blocks) + if (b.LineStartIndex == block.LineStartIndex) + throw new Exception(); + + blocks.Add(block); + FormatBlock(block, blocks.Count > 1 ? blocks[blocks.Count - 2] : null); + + charStart = i + 1; + lineStart += maxLineCountInBlock; + localLineCount = 1; + + if (i > lvchar) + break; + } + } + } + + // ----------------------------------------------------------- + // Rendering + // ----------------------------------------------------------- + + private void DrawBlocks() { + if (!IsLoaded || renderCanvas == null || lineNumbersCanvas == null) + return; + + var dc = renderCanvas.GetContext(); + var dc2 = lineNumbersCanvas.GetContext(); + for (int i = 0; i < blocks.Count; i++) { + InnerTextBlock block = blocks[i]; + Point blockPos = block.Position; + double top = blockPos.Y - VerticalOffset; + double bottom = top + blockHeight; + if (top < ActualHeight && bottom > 0) { + try { + dc.DrawText(block.FormattedText, new Point(2 - HorizontalOffset, block.Position.Y - VerticalOffset)); + if (IsLineNumbersMarginVisible) { + lineNumbersCanvas.Width = GetFormattedTextWidth(string.Format("{0:0000}", totalLineCount)) + 5; + dc2.DrawText(block.LineNumbers, new Point(lineNumbersCanvas.ActualWidth, 1 + block.Position.Y - VerticalOffset)); + } + } catch { + // Don't know why this exception is raised sometimes. + // Reproduce steps: + // - Sets a valid syntax highlighter on the box. + // - Copy a large chunk of code in the clipboard. + // - Paste it using ctrl+v and keep these buttons pressed. + } + } + } + dc.Close(); + dc2.Close(); + } + + // ----------------------------------------------------------- + // Utilities + // ----------------------------------------------------------- + + /// + /// Returns the index of the first visible text line. + /// + public int GetIndexOfFirstVisibleLine() { + int guessedLine = (int)(VerticalOffset / lineHeight); + return guessedLine > totalLineCount ? totalLineCount : guessedLine; + } + + /// + /// Returns the index of the last visible text line. + /// + public int GetIndexOfLastVisibleLine() { + double height = VerticalOffset + ViewportHeight; + int guessedLine = (int)(height / lineHeight); + return guessedLine > totalLineCount - 1 ? totalLineCount - 1 : guessedLine; + } + + /// + /// Formats and Highlights the text of a block. + /// + private void FormatBlock(InnerTextBlock currentBlock, InnerTextBlock previousBlock) { + currentBlock.FormattedText = GetFormattedText(currentBlock.RawText); + if (CurrentHighlighter != null) { + ThreadPool.QueueUserWorkItem(p => { + int previousCode = previousBlock != null ? previousBlock.Code : -1; + currentBlock.Code = CurrentHighlighter.Highlight(currentBlock.FormattedText, previousCode); + }); + } + } + + /// + /// Returns a formatted text object from the given string + /// + private FormattedText GetFormattedText(string text) { + FormattedText ft = new FormattedText( + text, + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + Brushes.Black, + 1) + { + Trimming = TextTrimming.None, + LineHeight = lineHeight + }; + + return ft; + } + + /// + /// Returns a string containing a list of numbers separated with newlines. + /// + private FormattedText GetFormattedLineNumbers(int firstIndex, int lastIndex) { + string text = ""; + for (int i = firstIndex + 1; i <= lastIndex + 1; i++) + text += i.ToString() + "\n"; + text = text.Trim(); + FormattedText ft = new FormattedText( + text, + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + new SolidColorBrush(Color.FromRgb(0x21, 0xA1, 0xD8)), + 1) + { + Trimming = TextTrimming.None, + LineHeight = lineHeight, + TextAlignment = TextAlignment.Right + }; + + return ft; + } + + /// + /// Returns the width of a text once formatted. + /// + private double GetFormattedTextWidth(string text) { + FormattedText ft = new FormattedText( + text, + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + Brushes.Black, + 1) + { + Trimming = TextTrimming.None, + LineHeight = lineHeight + }; + + return ft.Width; + } + + // ----------------------------------------------------------- + // Dependency Properties + // ----------------------------------------------------------- + + public static readonly DependencyProperty IsLineNumbersMarginVisibleProperty = DependencyProperty.Register( + "IsLineNumbersMarginVisible", typeof(bool), typeof(SyntaxHighlightBox), new PropertyMetadata(true)); + + public bool IsLineNumbersMarginVisible { + get { return (bool)GetValue(IsLineNumbersMarginVisibleProperty); } + set { SetValue(IsLineNumbersMarginVisibleProperty, value); } + } + + // ----------------------------------------------------------- + // Classes + // ----------------------------------------------------------- + + private class InnerTextBlock { + public string RawText { get; set; } + public FormattedText FormattedText { get; set; } + public FormattedText LineNumbers { get; set; } + public int CharStartIndex { get; private set; } + public int CharEndIndex { get; private set; } + public int LineStartIndex { get; private set; } + public int LineEndIndex { get; private set; } + public Point Position { get { return new Point(0, LineStartIndex * lineHeight); } } + public bool IsLast { get; set; } + public int Code { get; set; } + + private readonly double lineHeight; + + public InnerTextBlock(int charStart, int charEnd, int lineStart, int lineEnd, double lineHeight) { + CharStartIndex = charStart; + CharEndIndex = charEnd; + LineStartIndex = lineStart; + LineEndIndex = lineEnd; + this.lineHeight = lineHeight; + IsLast = false; + + } + + public string GetSubString(string text) { + return text.Substring(CharStartIndex, CharEndIndex - CharStartIndex + 1); + } + + public override string ToString() { + return string.Format("L:{0}/{1} C:{2}/{3} {4}", + LineStartIndex, + LineEndIndex, + CharStartIndex, + CharEndIndex, + FormattedText.Text); + } + } + } +} diff --git a/AurelienRibon.Ui.SyntaxHighlightBox/src/TextUtilities.cs b/AurelienRibon.Ui.SyntaxHighlightBox/src/TextUtilities.cs new file mode 100644 index 0000000..b17899d --- /dev/null +++ b/AurelienRibon.Ui.SyntaxHighlightBox/src/TextUtilities.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics.Contracts; + +namespace AurelienRibon.Ui.SyntaxHighlightBox { + public class TextUtilities { + /// + /// Returns the raw number of the current line count. + /// + public static int GetLineCount(String text) { + int lcnt = 1; + for (int i = 0; i < text.Length; i++) { + if (text[i] == '\n') + lcnt += 1; + } + return lcnt; + } + + /// + /// Returns the index of the first character of the + /// specified line. If the index is greater than the current + /// line count, the method returns the index of the last + /// character. The line index is zero-based. + /// + public static int GetFirstCharIndexFromLineIndex(string text, int lineIndex) { + if (text == null) + throw new ArgumentNullException("text"); + if (lineIndex <= 0) + return 0; + + int currentLineIndex = 0; + for (int i = 0; i < text.Length - 1; i++) { + if (text[i] == '\n') { + currentLineIndex += 1; + if (currentLineIndex == lineIndex) + return Math.Min(i + 1, text.Length - 1); + } + } + + return Math.Max(text.Length - 1, 0); + } + + /// + /// Returns the index of the last character of the + /// specified line. If the index is greater than the current + /// line count, the method returns the index of the last + /// character. The line-index is zero-based. + /// + public static int GetLastCharIndexFromLineIndex(string text, int lineIndex) { + if (text == null) + throw new ArgumentNullException("text"); + if (lineIndex < 0) + return 0; + + int currentLineIndex = 0; + for (int i = 0; i < text.Length - 1; i++) { + if (text[i] == '\n') { + if (currentLineIndex == lineIndex) + return i; + currentLineIndex += 1; + } + } + + return Math.Max(text.Length - 1, 0); + } + } +} diff --git a/Silver.sln b/Silver.sln new file mode 100644 index 0000000..0273b00 --- /dev/null +++ b/Silver.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.136 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silver", "Silver\Silver.csproj", "{DB4BA473-877B-4732-BDA0-125A06EA0696}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AurelienRibon.Ui.SyntaxHighlightBox", "AurelienRibon.Ui.SyntaxHighlightBox\AurelienRibon.Ui.SyntaxHighlightBox.csproj", "{10A9ECD3-AE1E-494D-9A27-8A32DD581759}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB4BA473-877B-4732-BDA0-125A06EA0696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB4BA473-877B-4732-BDA0-125A06EA0696}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB4BA473-877B-4732-BDA0-125A06EA0696}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB4BA473-877B-4732-BDA0-125A06EA0696}.Release|Any CPU.Build.0 = Release|Any CPU + {10A9ECD3-AE1E-494D-9A27-8A32DD581759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10A9ECD3-AE1E-494D-9A27-8A32DD581759}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10A9ECD3-AE1E-494D-9A27-8A32DD581759}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10A9ECD3-AE1E-494D-9A27-8A32DD581759}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F12A0FE5-346F-4638-9227-BCB3068A040A} + EndGlobalSection +EndGlobal diff --git a/Silver/App.config b/Silver/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/Silver/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Silver/App.xaml b/Silver/App.xaml new file mode 100644 index 0000000..5c33915 --- /dev/null +++ b/Silver/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Silver/App.xaml.cs b/Silver/App.xaml.cs new file mode 100644 index 0000000..b1e3826 --- /dev/null +++ b/Silver/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Silver +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/Silver/FSCollection.cs b/Silver/FSCollection.cs new file mode 100644 index 0000000..908fb19 --- /dev/null +++ b/Silver/FSCollection.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Silver +{ + class FSCollection : IDisposable + { + SortedList> Directories = new SortedList>(); + SortedList Files = new SortedList(); + + public FSCollection() + { + + } + + public void Add(string key, T value) => Add(Helpers.GetKeys(key), 0, value); + + public void Add(string[] keys, T value) => Add(keys, 0, value); + + public void Add(string[] keys, int offset, T value) + { + if (offset >= keys.Length) + { + throw new ArgumentOutOfRangeException("Offset must be less than the key length"); + } + if (offset == keys.Length - 1) + { + Files[keys[offset]] = value; + } + else + { + if (!Directories.ContainsKey(keys[offset])) + { + Directories[keys[offset]] = new FSCollection(); + } + Directories[keys[offset]].Add(keys, ++offset, value); + } + } + + public FSCollection GetDirectory(string key) => GetDirectory(Helpers.GetKeys(key), 0); + + public FSCollection GetDirectory(string[] keys) => GetDirectory(keys, 0); + + public FSCollection GetDirectory(string[] keys, int offset) + { + if (offset >= keys.Length) + { + return this; + } + if (offset == keys.Length - 1) + { + return Directories[keys[offset]]; + } + return Directories[keys[offset]].GetDirectory(keys, ++offset); + } + + public T Get(string key) => Get(Helpers.GetKeys(key), 0); + + public T Get(string[] keys) => Get(keys, 0); + + public T Get(string[] keys, int offset) + { + if (offset >= keys.Length) + { + throw new ArgumentOutOfRangeException("Offset must be less than the key length"); + } + if (offset == keys.Length - 1) + { + if (Files.TryGetValue(keys[offset], out var file)) + { + return file; + } + return default; + } + if (Directories.TryGetValue(keys[offset], out var dir)) + { + return dir.Get(keys, ++offset); + } + return default; + } + + private IEnumerable> SearchInternal(string inp, string delimeter, string curPath, StringComparison comparison) + { + foreach(var kv in Directories) + { + if ((curPath + delimeter + kv.Key).IndexOf(inp, comparison) >= 0) + { + yield return new KeyValuePair<(string Path, string Name), object>((curPath, kv.Key), kv.Value); + } + else + { + foreach (var i in kv.Value.SearchInternal(inp, delimeter, curPath + delimeter + kv.Key, comparison)) yield return i; + } + } + foreach (var kv in Files) + { + if ((curPath + delimeter + kv.Key).IndexOf(inp, comparison) >= 0) + { + yield return new KeyValuePair<(string Path, string Name), object>((curPath, kv.Key), kv.Value); + } + } + } + + public IEnumerable> Search(string inp, string delimeter = "/", StringComparison comparison = StringComparison.InvariantCultureIgnoreCase) => + SearchInternal(inp, delimeter, "", comparison); + + + public T this[string Path] + { + get => Get(Path); + set => Add(Path, value); + } + + public IEnumerator> GetEnumerator() + { + foreach (var kv in Directories) + { + yield return new KeyValuePair(kv.Key, kv.Value); + } + foreach (var kv in Files) + { + yield return new KeyValuePair(kv.Key, kv.Value); + } + } + + public IEnumerator>> EnumDirectories() => Directories.GetEnumerator(); + + public IEnumerator> EnumFiles() => Files.GetEnumerator(); + + public void Dispose() + { + Files.Clear(); + Directories.TrimExcess(); + Files = null; + foreach(var dir in Directories.Values) + { + dir.Dispose(); + } + Directories.Clear(); + Directories.TrimExcess(); + Directories = null; + } + } +} diff --git a/Silver/FileViewer.xaml b/Silver/FileViewer.xaml new file mode 100644 index 0000000..7a62b1a --- /dev/null +++ b/Silver/FileViewer.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Silver/FileViewer.xaml.cs b/Silver/FileViewer.xaml.cs new file mode 100644 index 0000000..cdf170f --- /dev/null +++ b/Silver/FileViewer.xaml.cs @@ -0,0 +1,144 @@ +using AurelienRibon.Ui.SyntaxHighlightBox; +using Newtonsoft.Json; +using PakReader; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Silver +{ + /// + /// Interaction logic for FileViewer.xaml + /// + public partial class FileViewer : Window + { + ObservableCollection Exports = new ObservableCollection(); + + Button Selected; + + public FileViewer(string title, ExportObject[] exps) + { + InitializeComponent(); + ExportPanel.ItemsSource = Exports; + Exports.Add(new ExportItem("Encoded Data", ExportItem.JsonImage, ExportType.JSON)); + Title = title; + + List ExtraExports = new List(); + foreach (var exp in exps) + { + switch (exp) + { + case Texture2D tex: + var image = tex.GetImage(); + Exports.Add(new ExportItem($"{image.Width}x{image.Height}", image, ExportType.IMAGE)); + break; + case UObject obj: + ExtraExports.Add(obj); + break; + default: + break; + } + } + + JsonTxt.CurrentHighlighter = HighlighterManager.Instance.Highlighters["JSON"]; + JsonTxt.Text = JsonConvert.SerializeObject(ExtraExports, Formatting.Indented); + } + + private void SelectExport(object sender, RoutedEventArgs e) + { + var btn = sender as Button; + Selected.IsEnabled = true; + btn.IsEnabled = false; + btn.DataContext = SelectExport(btn.DataContext as ExportItem); + Selected = btn; + } + + ExportItem SelectExport(ExportItem obj) + { + var selectedObj = Selected.DataContext as ExportItem; + switch (selectedObj.Type) + { + case ExportType.JSON: + JsonTxt.Visibility = Visibility.Collapsed; + break; + case ExportType.IMAGE: + ImagePanelBg.Visibility = Visibility.Collapsed; + ImagePanel.Visibility = Visibility.Collapsed; + break; + case ExportType.OPENGL: + break; + } + switch (obj.Type) + { + case ExportType.JSON: + JsonTxt.Visibility = Visibility.Visible; + break; + case ExportType.IMAGE: + ImagePanelBg.Visibility = Visibility.Visible; + ImagePanel.Visibility = Visibility.Visible; + ImagePanel.Source = obj.Thumbnail; + break; + case ExportType.OPENGL: + break; + } + Selected.DataContext = selectedObj; + return obj; + } + + private void LoadedExport(object sender, RoutedEventArgs e) + { + if (Selected == null) + { + Selected = sender as Button; + Selected.IsEnabled = false; + } + } + + class ExportItem + { + internal readonly static BitmapImage JsonImage; + + static ExportItem() + { + JsonImage = new BitmapImage(); + JsonImage.BeginInit(); + JsonImage.UriSource = new Uri(@"/Silver;component/Resources/json.png", UriKind.Relative); + JsonImage.EndInit(); + } + + public string Caption { get; set; } + public ImageSource Thumbnail { get; set; } + public ExportType Type; + + public ExportItem(string caption, SKImage thumbnail, ExportType type) + { + Caption = caption; + using (var data = thumbnail.Encode()) + using (var stream = data.AsStream()) + { + Thumbnail = Helpers.GetImageSource(stream); + } + Type = type; + } + + public ExportItem(string caption, ImageSource source, ExportType type) + { + Caption = caption; + Thumbnail = source; + Type = type; + } + } + + enum ExportType + { + JSON, + IMAGE, + OPENGL // Unused + } + } +} diff --git a/Silver/FodyWeavers.xml b/Silver/FodyWeavers.xml new file mode 100644 index 0000000..a5dcf04 --- /dev/null +++ b/Silver/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Silver/FodyWeavers.xsd b/Silver/FodyWeavers.xsd new file mode 100644 index 0000000..44a5374 --- /dev/null +++ b/Silver/FodyWeavers.xsd @@ -0,0 +1,111 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Silver/Helpers.cs b/Silver/Helpers.cs new file mode 100644 index 0000000..b8a7e48 --- /dev/null +++ b/Silver/Helpers.cs @@ -0,0 +1,160 @@ +using Microsoft.Win32; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using DialogResult = System.Windows.Forms.DialogResult; +using FolderBrowserDialog = System.Windows.Forms.FolderBrowserDialog; + +namespace Silver +{ + static class Helpers + { + public static (string Path, string Extension) GetPath(string inp) + { + int extInd = inp.LastIndexOf('.'); + return (inp.Substring(0, extInd), inp.Substring(extInd + 1)); + } + + public static byte[] StringToByteArray(string hex) + { + if (hex.Length % 2 == 1) + throw new Exception("The binary key cannot have an odd number of digits"); + + byte[] arr = new byte[hex.Length >> 1]; + + for (int i = 0; i < hex.Length >> 1; ++i) + { + arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + GetHexVal(hex[(i << 1) + 1])); + } + + return arr; + } + + public static int GetHexVal(char hex) + { + //For uppercase A-F letters: + //return val - (val < 58 ? 48 : 55); + //For lowercase a-f letters: + return hex - (hex < 58 ? 48 : 87); + //Or the two combined, but a bit slower: + //return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); + } + + static readonly uint[] _Lookup32 = Enumerable.Range(0, 256).Select(i => { + string s = i.ToString("x2"); + return s[0] + ((uint)s[1] << 16); + }).ToArray(); + public static string ToHex(byte[] bytes) + { + var result = new char[bytes.Length * 2]; + for (int i = 0; i < bytes.Length; i++) + { + var val = _Lookup32[bytes[i]]; + result[2 * i] = (char)val; + result[2 * i + 1] = (char)(val >> 16); + } + return new string(result); + } + + const char DirSeparator = '/'; + public static string[] GetKeys(string path) + { + if (path.Length == 0 || (path[0] == DirSeparator && path.Length == 1)) + { + return new string[0]; + } + if (path[0] == DirSeparator) + { + path = path.Substring(1); + } + if (path[path.Length - 1] == DirSeparator) + { + path = path.Substring(0, path.Length - 1); + } + return path.Replace(DirSeparator + DirSeparator.ToString(), DirSeparator.ToString()).Split(DirSeparator); + } + + public static string ReadFString(this BinaryReader reader) + { + ushort length = reader.ReadUInt16(); + return Encoding.UTF8.GetString(reader.ReadBytes(length)); + } + + public static void WriteFString(this BinaryWriter writer, string value) + { + byte[] toWrite = Encoding.UTF8.GetBytes(value ?? ""); + writer.Write((ushort)toWrite.Length); + writer.Write(toWrite); + } + + public static MessageBoxResult AskConfirmation(this Window me, string caption, MessageBoxButton buttons = MessageBoxButton.OKCancel) + { + return MessageBox.Show(me, caption, "Silver", buttons); + } + + public static MessageBoxResult SaveFileCheck(this MainWindow me) + { + if (me.Project == null) return MessageBoxResult.No; + return AskConfirmation(me, $"Do you want to save changes to {me.Project?.Name ?? "Untitled Project"}?", MessageBoxButton.YesNoCancel); + } + + public static string ChooseSaveFile(string name, string extension) + { + var dialog = new SaveFileDialog + { + Filter = $"{name} (*.{extension})|*.{extension}", + DefaultExt = extension, + AddExtension = true + }; + if (dialog.ShowDialog() ?? false) + { + return dialog.FileName; + } + return null; + } + + public static string ChooseOpenFile(string name, string extension) + { + var dialog = new OpenFileDialog + { + Filter = $"{name} (*.{extension})|*.{extension}", + DefaultExt = extension, + AddExtension = true + }; + if (dialog.ShowDialog() ?? false) + { + return dialog.FileName; + } + return null; + } + + public static string ChooseFolder() + { + using (var dialog = new FolderBrowserDialog()) + { + if (dialog.ShowDialog() == DialogResult.OK) + { + return dialog.SelectedPath; + } + } + return null; + } + + public static ImageSource GetImageSource(Stream stream) + { + BitmapImage photo = new BitmapImage(); + using (stream) + { + photo.BeginInit(); + photo.CacheOption = BitmapCacheOption.OnLoad; + photo.StreamSource = stream; + photo.EndInit(); + } + return photo; + } + } +} diff --git a/Silver/MainWindow.xaml b/Silver/MainWindow.xaml new file mode 100644 index 0000000..8f93d5c --- /dev/null +++ b/Silver/MainWindow.xaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Silver/MainWindow.xaml.cs b/Silver/MainWindow.xaml.cs new file mode 100644 index 0000000..9ba2bfa --- /dev/null +++ b/Silver/MainWindow.xaml.cs @@ -0,0 +1,460 @@ +using PakReader; +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Silver +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public Project Project = new Project(); + public string ProjectPath; + public bool ProjectDirty = false; + public bool Searching; + string cwd_; + public string WorkingDir { get => cwd_; set { cwd_ = value; WorkingDirTxt.Text = value; } } + public PathHistory History = new PathHistory(50); + FSCollection Index = new FSCollection(); + + ObservableCollection Items = new ObservableCollection(); + + public MainWindow() + { + InitializeComponent(); + DataContext = new Context(this); + Title = "Silver - Untitled Project"; + } + + private void Click_New(object sender, RoutedEventArgs e) + { + if (ProjectDirty) + { + switch (this.SaveFileCheck()) + { + case MessageBoxResult.Yes: + string file = Helpers.ChooseSaveFile("Silver Project File", "slv"); + if (file != null) + { + Project.Save(file); + } + else // Cancel or X + { + return; + } + break; + case MessageBoxResult.No: + break; + case MessageBoxResult.Cancel: + return; + } + } + ProjectPath = null; + Project = new Project(); + ProjectDirty = false; + } + + private void Click_Open(object sender, RoutedEventArgs e) + { + if (ProjectDirty) + { + switch (this.SaveFileCheck()) + { + case MessageBoxResult.Yes: + string file = Helpers.ChooseSaveFile("Silver Project File", "slv"); + if (file != null) + { + Project.Save(file); + } + else // Cancel or X + { + return; + } + break; + case MessageBoxResult.No: + break; + case MessageBoxResult.Cancel: + return; + } + } + string fileName = Helpers.ChooseOpenFile("Silver Project File", "slv"); + if (fileName != null) + { + ProjectPath = fileName; + Project = new Project(fileName); + LoadProject(); + Refresh(); + } + else + { + ProjectPath = null; + Project = new Project(); + } + ProjectDirty = false; + } + + private void Click_Save(object sender, RoutedEventArgs e) + { + if (ProjectPath == null) + { + ProjectPath = Helpers.ChooseSaveFile("Silver Project File", "slv"); + if (ProjectPath == null) + { + return; + } + } + Project.Save(ProjectPath); + ProjectDirty = false; + } + + private void Click_SaveAs(object sender, RoutedEventArgs e) + { + string file = Helpers.ChooseSaveFile("Silver Project File", "slv"); + if (file == null) + { + return; + } + ProjectPath = file; + Project.Save(ProjectPath); + ProjectDirty = false; + } + + private void Click_Exit(object sender, RoutedEventArgs e) + { + if (ProjectDirty) + { + switch (this.SaveFileCheck()) + { + case MessageBoxResult.Yes: + string file = Helpers.ChooseSaveFile("Silver Project File", "slv"); + if (file != null) + { + Project.Save(file); + } + else // Cancel or X + { + return; + } + break; + case MessageBoxResult.No: + break; + case MessageBoxResult.Cancel: + return; + } + } + Application.Current.Shutdown(); + } + + private void Click_Find(object sender, RoutedEventArgs e) + { + FilterTxt.Focus(); + } + + private void Click_EditProject(object sender, RoutedEventArgs e) + { + var window = new ProjectProps(Project); + window.ShowDialog(); + if (window.Dirty) + { + LoadProject(); + Refresh(); + } + } + + private void Click_Settings(object sender, RoutedEventArgs e) + { + // Settings window not added yet + } + + private void Click_Exports(object sender, RoutedEventArgs e) + { + // Export window not added yet + } + + private void Click_Search(object sender, RoutedEventArgs e) + { + if (Searching) + { + Items.Clear(); + foreach (var entry in Index.GetDirectory(WorkingDir).Search(FilterTxt.Text)) + { + PakPackage file = entry.Value as PakPackage; + Items.Add(new PanelItem() + { + IsDirectory = file == null, + Name = WorkingDir + (string.IsNullOrEmpty(entry.Key.Path) ? "" : entry.Key.Path.Substring(1) + "/") + entry.Key.Name, + Size = file != null ? file.uasset?.UncompressedSize ?? 0 + file.uexp?.UncompressedSize ?? 0 + file.ubulk?.UncompressedSize ?? 0 : 0, + Assets = file?.ubulk != null ? "Bulk" : null, + Openable = file == null || (file.uasset != null && file.uexp != null) + }); + } + WorkingDir = "Search Results for " + FilterTxt.Text; + } + } + + private void FilterTxt_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Return) + { + e.Handled = true; + Searching = !string.IsNullOrWhiteSpace(FilterTxt.Text); + Click_Search(null, null); + } + } + + private void Click_Up(object sender, RoutedEventArgs e) + { + if (Searching) + { + Searching = false; + WorkingDir = History.MoveBack(); + Refresh(); + return; + } + if (!string.IsNullOrEmpty(WorkingDir) && WorkingDir != "/") + { + WorkingDir = History.Navigate(WorkingDir.Substring(0, WorkingDir.LastIndexOf('/', WorkingDir.Length - 2)) + '/'); + Refresh(); + } + } + + private void Click_Back(object sender, RoutedEventArgs e) + { + if (WorkingDir == null) + return; + if (!Searching) + WorkingDir = History.MoveBack(); + Refresh(); + } + + private void Click_Forward(object sender, RoutedEventArgs e) + { + if (WorkingDir == null) + return; + WorkingDir = History.MoveForward(); + Refresh(); + } + + private void GotFocusPlaceholder(object sender, RoutedEventArgs e) + { + var textbox = sender as TextBox; + if (textbox.Text == "Search" && textbox.Foreground == Brushes.Gray) + { + textbox.Text = ""; + textbox.Foreground = Brushes.Black; + } + } + + private void LostFocusPlaceholder(object sender, RoutedEventArgs e) + { + var textbox = sender as TextBox; + if (textbox.Text == "") + { + textbox.Foreground = Brushes.Gray; + textbox.Text = "Search"; + } + } + + void Refresh() + { + Items.Clear(); + var dir = Index.GetDirectory(WorkingDir.Substring(1)); + if (dir != null) + { + foreach(var entry in dir) + { + PakPackage file = entry.Value as PakPackage; + Items.Add(new PanelItem() + { + IsDirectory = file == null, + Name = entry.Key, + Size = file != null ? file.uasset?.UncompressedSize ?? 0 + file.uexp?.UncompressedSize ?? 0 + file.ubulk?.UncompressedSize ?? 0 : 0, + Assets = file?.ubulk != null ? "Bulk" : null, + Openable = file == null || (file.uasset != null && file.uexp != null) + }); + } + } + } + + void LoadProject() + { + Title = "Silver - " + Project.Name; + History.Clear(); + WorkingDir = History.Navigate("/"); + FilePanel.ItemsSource = Items; + foreach (var file in Project.Files) + { + PakReader.PakReader reader; + if (file.Index != null) + { + if (file.Index.Type == ProjectFileIndex.IndexType.FILE_INFO) + { + reader = new PakReader.PakReader(file.Path, file.Key, false); + foreach (var entry in file.Index.Index) + { + var (Path, Extension) = Helpers.GetPath(entry.Name); + var package = Index[file.MountPoint + Path] ?? new PakPackage(); + switch (Extension) + { + case "uasset": + package.uasset = entry.Info; + package.AssetReader = reader; + break; + case "uexp": + package.uexp = entry.Info; + package.ExpReader = reader; + break; + case "ubulk": + package.ubulk = entry.Info; + package.BulkReader = reader; + break; + case "umap": + break; + default: + //Console.WriteLine($"Unknown extension: {Extension} in {Path}"); + break; + } + Index[file.MountPoint + Path] = package; + } + }/* + else if (file.Index.Type == ProjectFileIndex.IndexType.FILE_NAME) + { + reader = new PakReader.PakReader(file.Path, file.Key, false); + foreach (var entry in file.Index.Index) + { + var path = Helpers.GetPath(entry.Name); + var package = Index[file.MountPoint + path.Path] ?? new PakPackage(); + switch (path.Extension) + { + case "uasset": + package.AssetReader = reader; + break; + case "uexp": + package.ExpReader = reader; + break; + case "ubulk": + package.BulkReader = reader; + break; + default: + Console.WriteLine($"Unknown extension: {path.Extension} in {path.Path}"); + break; + } + Index[file.MountPoint + path.Path] = package; + } + }*/ + } + else + { + reader = new PakReader.PakReader(file.Path, file.Key); + foreach (var entry in reader.FileInfos) + { + var (Path, Extension) = Helpers.GetPath(entry.Name); + var package = Index[file.MountPoint + Path] ?? new PakPackage(); + switch (Extension) + { + case "uasset": + package.uasset = entry; + package.AssetReader = reader; + break; + case "uexp": + package.uexp = entry; + package.ExpReader = reader; + break; + case "ubulk": + package.ubulk = entry; + package.BulkReader = reader; + break; + case "umap": + break; + default: + //Console.WriteLine($"Unknown extension: {Extension} in {Path}"); + break; + } + Index[file.MountPoint + Path] = package; + } + } + } + } + + private void Entry_DoubleClick(object sender, MouseButtonEventArgs e) + { + PanelItem Context = (PanelItem)((DataGridRow)sender).DataContext; + if (!Context.Openable) return; + if (Searching) + { + if (Context.IsDirectory) + { + WorkingDir = History.Navigate(Context.Name + "/"); + Searching = false; + Refresh(); + } + else + { + OpenViewer(Context.Name, Index[Context.Name]); + } + return; + } + if (Context.IsDirectory) + { + WorkingDir += Context.Name + "/"; + History.Navigate(WorkingDir); + Refresh(); + } + else + { + OpenViewer(Context.Name, Index[WorkingDir + Context.Name]); + } + } + + public void OpenViewer(string fileName, PakPackage package) + { + foreach (var exp in package.Exports) + { + if (exp is Texture2D) + { + var tex = exp as Texture2D; + tex.GetImage(); + } + } + new FileViewer(fileName, package.Exports).ShowDialog(); + } + + class Context + { + MainWindow Window; + public Context(MainWindow window) + { + Window = window; + } + public ICommand New => new CommandHandler(() => Window.Click_New(null, null)); + public ICommand Open => new CommandHandler(() => Window.Click_Open(null, null)); + public ICommand Save => new CommandHandler(() => Window.Click_Save(null, null)); + public ICommand SaveAs => new CommandHandler(() => Window.Click_SaveAs(null, null)); + public ICommand Find => new CommandHandler(() => Window.Click_Find(null, null)); + public ICommand EditProject => new CommandHandler(() => Window.Click_EditProject(null, null)); + } + + class CommandHandler : ICommand + { + private readonly Action _action; + public CommandHandler(Action action) + { + _action = action; + } + + public bool CanExecute(object parameter) => true; + + public event EventHandler CanExecuteChanged; + + public void Execute(object parameter) + { + _action(); + } + } + } +} diff --git a/Silver/PanelItem.cs b/Silver/PanelItem.cs new file mode 100644 index 0000000..c745190 --- /dev/null +++ b/Silver/PanelItem.cs @@ -0,0 +1,66 @@ +using System; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Silver +{ + class PanelItem + { + static PanelItem() + { + DirImage = new BitmapImage(); + DirImage.BeginInit(); + DirImage.UriSource = new Uri(@"/Silver;component/Resources/folder.ico", UriKind.Relative); + DirImage.EndInit(); + + FileImage = new BitmapImage(); + FileImage.BeginInit(); + FileImage.UriSource = new Uri(@"/Silver;component/Resources/file.ico", UriKind.Relative); + FileImage.EndInit(); + } + + readonly static BitmapImage DirImage; + readonly static BitmapImage FileImage; + + public bool Checked { get; set; } + public ImageSource Pic => IsDirectory ? DirImage : FileImage; + public string ReadableSize + { + get + { + if (Size == 0) return null; + long absolute_i = Size < 0 ? -Size : Size; + string suffix; + double readable; + if (absolute_i >= 0x40000000) + { + suffix = "GB"; + readable = Size >> 20; + } + else if (absolute_i >= 0x100000) + { + suffix = "MB"; + readable = Size >> 10; + } + else if (absolute_i >= 0x400) + { + suffix = "KB"; + readable = Size; + } + else + { + return Size.ToString("0 B"); + } + readable = readable / 1024; + return readable.ToString("0.## ") + suffix; + } + } + + public bool IsDirectory { get; set; } + public string Name { get; set; } + public long Size { get; set; } + public string Assets { get; set; } + + public bool Openable { get; set; } + } +} diff --git a/Silver/PathHistory.cs b/Silver/PathHistory.cs new file mode 100644 index 0000000..7de915c --- /dev/null +++ b/Silver/PathHistory.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace Silver +{ + public class PathHistory + { + LinkedList History = new LinkedList(); + LinkedListNode CurrentPoint; + readonly int MaxEntries; + + public PathHistory(int maxEntries) + { + MaxEntries = maxEntries; + } + + public void Clear() + { + History.Clear(); + CurrentPoint = null; + } + + public string Navigate(string path) + { + if (CurrentPoint != null && History.Last != CurrentPoint) + { + while (CurrentPoint.Next != null) + { + CurrentPoint = CurrentPoint.Next; + History.Remove(CurrentPoint.Previous); + } + } + CurrentPoint = History.AddLast(path); + if (History.Count > MaxEntries) + { + History.RemoveFirst(); + } + return path; + } + + public string MoveBack() + { + if (CurrentPoint != History.First) + { + CurrentPoint = CurrentPoint.Previous; + } + return CurrentPoint.Value; + } + + public string MoveForward() + { + if (CurrentPoint != History.Last) + { + CurrentPoint = CurrentPoint.Next; + } + return CurrentPoint.Value; + } + } +} diff --git a/Silver/Project.cs b/Silver/Project.cs new file mode 100644 index 0000000..bc62a20 --- /dev/null +++ b/Silver/Project.cs @@ -0,0 +1,250 @@ +using PakReader; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Silver +{ + public class Project + { + public readonly ushort Version; + public string Name; + public readonly List Files; + + public Project(string path) : this(File.OpenRead(path)) { } + + public Project(Stream stream) + { + using (stream) + using (var reader = new BinaryReader(stream)) + { + Version = reader.ReadUInt16(); + Name = reader.ReadFString(); + Files = new List(reader.ReadUInt16()); + for (int i = 0; i < Files.Capacity; i++) + { + Files.Add(new ProjectFile(reader)); + } + } + } + + // New project + public Project() + { + Version = 1; + Name = "Untitled Project"; + Files = new List(); + } + + public void Save(string path) => Save(File.OpenWrite(path)); + + public void Save(Stream stream) + { + if (!stream.CanWrite) + { + throw new ArgumentException("Can't write to the stream"); + } + using (var writer = new BinaryWriter(stream)) + { + writer.Write(Version); + writer.WriteFString(Name); + writer.Write((ushort)Files.Count); + foreach(var f in Files) + { + f.Write(writer); + } + } + } + } + + public class ProjectFile + { + public readonly string Path; + public readonly byte[] Hash; // unused + public readonly byte[] Key; + public readonly string MountPoint; + public readonly ProjectFileIndex Index; // unused + + public ProjectFile(string path, byte[] key) + { + Path = path; + Key = key; + } + + public ProjectFile(BinaryReader reader) + { + Path = reader.ReadFString(); + MountPoint = reader.ReadFString(); + if (reader.ReadBoolean()) + { + Hash = reader.ReadBytes(32); + } + if (reader.ReadBoolean()) + { + Key = reader.ReadBytes(32); + } + if (reader.ReadBoolean()) + { + Index = new ProjectFileIndex(reader); + } + } + + public void Write(BinaryWriter writer) + { + writer.WriteFString(Path); + writer.WriteFString(MountPoint); + if (Hash != null) + { + writer.Write(true); + writer.Write(Hash); + } + else + { + writer.Write(false); + } + if (Key != null) + { + writer.Write(true); + writer.Write(Key); + } + else + { + writer.Write(false); + } + if (Index != null) + { + writer.Write(true); + Index.Write(writer); + } + else + { + writer.Write(false); + } + } + } + + public class ProjectFileIndex + { + public readonly CompressionType Compression; + public readonly IndexType Type; + public readonly Entry[] Index; + + public ProjectFileIndex(BinaryReader reader) + { + Compression = (CompressionType)reader.ReadByte(); + switch (Compression) + { + case CompressionType.NONE: + break; + default: + throw new NotImplementedException(); + } + Type = (IndexType)reader.ReadByte(); + Index = new Entry[reader.ReadInt32()]; + for(int i = 0; i < Index.Length; i++) + { + Index[i] = new Entry(reader, Type); + } + } + + public void Write(BinaryWriter writer) + { + writer.Write((byte)Compression); + switch (Compression) + { + case CompressionType.NONE: + break; + default: + throw new NotImplementedException(); + } + writer.Write((byte)Type); + writer.Write(Index.Length); + foreach(var e in Index) + { + e.Write(writer, Type); + } + } + + public class Entry + { + public readonly string Name; + + public readonly EntryInfo Info; + + public Entry(BinaryReader reader, IndexType type) + { + switch (type) + { + /* + case IndexType.FILE_NAME: + Name = reader.ReadFString(); + break; + */ + case IndexType.FILE_INFO: + Name = reader.ReadFString(); + Info = new EntryInfo(reader); + break; + default: + throw new ArgumentException($"Index type ({type}) is invalid"); + } + } + + public void Write(BinaryWriter writer, IndexType type) + { + switch (type) + { + /* + case IndexType.FILE_NAME: + writer.WriteFString(Name); + break; + */ + case IndexType.FILE_INFO: + writer.WriteFString(Name); + Info.Write(writer); + break; + default: + throw new ArgumentException($"Index type ({type}) is invalid"); + } + } + } + + public class EntryInfo : BasePakEntry + { + public EntryInfo(BinaryReader reader) + { + Pos = reader.ReadInt64(); + Size = reader.ReadInt64(); + StructSize = reader.ReadInt32(); + UncompressedSize = reader.ReadInt64(); + Encrypted = reader.ReadBoolean(); + } + + public void Write(BinaryWriter writer) + { + writer.Write(Pos); + writer.Write(Size); + writer.Write(StructSize); + writer.Write(UncompressedSize); + writer.Write(Encrypted); + } + } + + public enum CompressionType : byte + { + NONE, + GZIP, + BROTLI, + BZIP2, + DEFLATE, + LZMA2, + // LZMA + // PPMD + } + + public enum IndexType : byte + { + // FILE_NAME, // Not supported yet + FILE_INFO + } + } +} diff --git a/Silver/ProjectProps.xaml b/Silver/ProjectProps.xaml new file mode 100644 index 0000000..dbcd65e --- /dev/null +++ b/Silver/ProjectProps.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + +