diff --git a/.editorconfig b/.editorconfig
index 96594f0..88a14c3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,57 +1,10 @@
-[*.cs]
+[*.{cs,vb}]
-# CS8602: Dereference of a possibly null reference.
-dotnet_diagnostic.CS8602.severity = suggestion
-csharp_indent_labels = one_less_than_current
-csharp_using_directive_placement = outside_namespace:silent
-csharp_prefer_simple_using_statement = true:suggestion
-csharp_prefer_braces = true:silent
-csharp_style_namespace_declarations = file_scoped:silent
-csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_expression_bodied_methods = false:silent
-csharp_style_expression_bodied_constructors = false:silent
-csharp_style_expression_bodied_operators = false:silent
-csharp_style_expression_bodied_properties = true:silent
-csharp_style_expression_bodied_indexers = true:silent
-csharp_style_expression_bodied_accessors = true:silent
-csharp_style_expression_bodied_lambdas = true:silent
-csharp_style_expression_bodied_local_functions = false:silent
-csharp_style_throw_expression = true:suggestion
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
-csharp_prefer_static_local_function = true:suggestion
-csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_conditional_delegate_call = true:suggestion
-csharp_style_prefer_parameter_null_checking = true:suggestion
-csharp_style_prefer_switch_expression = true:suggestion
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_extended_property_pattern = true:suggestion
-csharp_style_var_for_built_in_types = false:silent
-csharp_style_var_when_type_is_apparent = false:silent
-csharp_style_var_elsewhere = false:silent
-csharp_style_prefer_top_level_statements = true:silent
-dotnet_diagnostic.IDE0005.severity = suggestion
-dotnet_diagnostic.IDE0063.severity = warning
-dotnet_diagnostic.IDE0065.severity = warning
-dotnet_diagnostic.CA1001.severity = warning
-dotnet_diagnostic.CA1309.severity = silent
-dotnet_diagnostic.CA1805.severity = warning
-dotnet_diagnostic.IDE0036.severity = suggestion
-dotnet_diagnostic.IDE0060.severity = warning
+# IDE0017: Simplify object initialization
+dotnet_diagnostic.IDE0017.severity = silent
+
+# CA1416: Validate platform compatibility
+dotnet_diagnostic.CA1416.severity = none
[*.{cs,vb}]
#### Naming styles ####
@@ -66,10 +19,6 @@ dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
-dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
-dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
-
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
@@ -80,10 +29,6 @@ dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
-dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
-dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.non_field_members.required_modifiers =
-
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
@@ -95,15 +40,6 @@ dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
-
-dotnet_naming_style.pascal_case.required_prefix =
-dotnet_naming_style.pascal_case.required_suffix =
-dotnet_naming_style.pascal_case.word_separator =
-dotnet_naming_style.pascal_case.capitalization = pascal_case
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-tab_width = 4
-indent_size = 4
-end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
@@ -119,23 +55,27 @@ dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
-dotnet_style_readonly_field = true:suggestion
-dotnet_style_predefined_type_for_locals_parameters_members = true:silent
-dotnet_style_predefined_type_for_member_access = true:silent
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
-dotnet_style_allow_multiple_blank_lines_experimental = true:silent
-dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
-dotnet_code_quality_unused_parameters = all:suggestion
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-dotnet_style_qualification_for_field = false:silent
-dotnet_style_qualification_for_property = false:silent
-dotnet_style_qualification_for_method = false:silent
-dotnet_style_qualification_for_event = false:silent
-dotnet_diagnostic.CA1000.severity = suggestion
-dotnet_diagnostic.CA1036.severity = error
-dotnet_diagnostic.CA1707.severity = suggestion
-dotnet_diagnostic.CA1711.severity = suggestion
-dotnet_diagnostic.CA1710.severity = warning
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+
+[*.cs]
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:silent
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_indent_labels = flush_left
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9491a2f..cc858b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
+Internals*/
+
*.rsuser
*.suo
*.user
diff --git a/README.md b/README.md
index 6a0762a..8a0c82b 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,31 @@
## 📌 rowsSharp
-C#/WPF-based CSV filtering and editing tool. Rewritten from [Rows](https://github.com/haruki-taka8/rows).
+C#/WPF-based CSV filtering and editing tool. Rewritten and enhanced from [Rows](https://github.com/haruki-taka8/rows).
## ✔️ Features
**Filtering**
* Display user-friendly aliases instead of raw data
- * Display a preview image based on the selected row
- * Conditional formatting support
- * Regular expressions support
+ * Display an image based on the selected row
+ * Conditional formatting
+ * Regular expressions
**Editing**
- * Add/remove rows
+ * Add/edit/remove rows and columns
* Add rows with a template
- * Change existing rows
* Unlimited undo & redo
+ * Shortcut keys
-## 🌎 Using
-* [CsvHelper](https://joshclose.github.io/CsvHelper/)
-* [Microsoft.Xaml.Behaviors.Wpf](https://github.com/microsoft/XamlBehaviorsWpf)
-* [NLog](https://nlog-project.org/)
+## 🌎 Dependencies
+* [CsvHelper](https://joshclose.github.io/CsvHelper/) ([License](https://github.com/JoshClose/CsvHelper/blob/master/LICENSE.txt))
+* [Material Symbols](https://github.com/marella/material-symbols/) ([License](https://github.com/marella/material-symbols/blob/main/LICENSE))
+ * Font was converted to `.otf` for WPF compatibility
+ * Font content is rotated and/or flipped inside the UI
+* [Microsoft.Xaml.Behaviors.Wpf](https://github.com/microsoft/XamlBehaviorsWpf) ([License](https://github.com/microsoft/XamlBehaviorsWpf/blob/master/LICENSE))
+* [NLog](https://nlog-project.org/) ([License](https://github.com/NLog/NLog/blob/dev/LICENSE.txt))
## 🧪 Building
-Official binaries are built with Visual Studio 2022 and .NET 6, using the _release_ configuration.
+Official binaries are built with Visual Studio 2022 and .NET 6 using the _release_ configuration.
diff --git a/rowsSharp/App.xaml b/rowsSharp/App.xaml
index c62d225..8a5fd5a 100644
--- a/rowsSharp/App.xaml
+++ b/rowsSharp/App.xaml
@@ -1,12 +1,24 @@
+ StartupUri="View/MainWindow.xaml">
+
+ pack://application,,,/View/Font/#Material Symbols Outlined 48pt
+
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rowsSharp/App.xaml.cs b/rowsSharp/App.xaml.cs
index c68ec6d..234ac5c 100644
--- a/rowsSharp/App.xaml.cs
+++ b/rowsSharp/App.xaml.cs
@@ -5,19 +5,17 @@ namespace rowsSharp;
public partial class App : Application
{
- internal static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
+ internal static NLog.Logger Logger => NLog.LogManager.GetCurrentClassLogger();
private App()
{
- AppDomain.CurrentDomain.UnhandledException += new(Handler);
+ AppDomain.CurrentDomain.UnhandledException += Handler;
}
private void Handler(object sender, UnhandledExceptionEventArgs args)
{
- var exception = (Exception)args.ExceptionObject;
-
MessageBox.Show(
- "RowsSharp will close due to the following exception:\n\n" + exception.Message + "\n\n" + exception.StackTrace,
+ "RowsSharp will close due to the following error:\n\n" + args.ExceptionObject,
"RowsSharp",
MessageBoxButton.OK,
MessageBoxImage.Error
diff --git a/rowsSharp/AssemblyInfo.cs b/rowsSharp/AssemblyInfo.cs
index 66a9ebf..03b1ce6 100644
--- a/rowsSharp/AssemblyInfo.cs
+++ b/rowsSharp/AssemblyInfo.cs
@@ -13,4 +13,4 @@
[assembly: AssemblyProduct("RowsSharp")]
[assembly: AssemblyCompany("haruki-taka8")]
[assembly: AssemblyCopyright("MIT License, see LICENSE file")]
-[assembly: AssemblyVersion("22.12.*")]
\ No newline at end of file
+[assembly: AssemblyVersion("23.06.*")]
\ No newline at end of file
diff --git a/rowsSharp/Internal/Command.cs b/rowsSharp/Command.cs
similarity index 55%
rename from rowsSharp/Internal/Command.cs
rename to rowsSharp/Command.cs
index 4d02870..2475a51 100644
--- a/rowsSharp/Internal/Command.cs
+++ b/rowsSharp/Command.cs
@@ -25,32 +25,43 @@ public event EventHandler? CanExecuteChanged
}
}
-public class DelegateCommand : ICommand where T : class
+/*
+ * The following constructors deliberately ALLOW the use of value type,
+ * despite Microsoft.Practices.Prism.Commands doing otherwise.
+ *
+ * It can be safely assumed that CanExecute(null) will not cause any
+ * issues.
+ */
+
+public class DelegateCommand : ICommand
{
- private readonly Predicate _canExecute;
+ private readonly Func _canExecute;
private readonly Action _execute;
- internal DelegateCommand(Action execute) : this(execute, (T obj) => true) { }
-
- internal DelegateCommand(Action execute, Predicate canExecute)
+ internal DelegateCommand(Action execute) : this(execute, (T parameter) => true) { }
+ internal DelegateCommand(Action execute, Func canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
- public bool CanExecute(object? parameter) =>
- _canExecute is not null
- && parameter is not null
- && _canExecute((T)parameter);
+ public bool CanExecute(object? parameter)
+ {
+ return parameter is T type
+ && _canExecute.Invoke(type);
+ }
public void Execute(object? parameter)
{
- if (parameter is not null) { _execute((T)parameter); }
+ if (parameter is T type)
+ {
+ _execute(type);
+ }
}
- public event EventHandler? CanExecuteChanged;
- public void RaiseCanExecuteChanged()
+ public event EventHandler? CanExecuteChanged
{
- if (CanExecuteChanged is not null) { CanExecuteChanged(this, EventArgs.Empty); }
+ add => CommandManager.RequerySuggested += value;
+ remove => CommandManager.RequerySuggested -= value;
}
}
diff --git a/rowsSharp/Domain/BaseDir.cs b/rowsSharp/Domain/BaseDir.cs
new file mode 100644
index 0000000..da98456
--- /dev/null
+++ b/rowsSharp/Domain/BaseDir.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace rowsSharp.Domain;
+
+internal static class BaseDir
+{
+ private const string Metavariable = "$baseDir";
+ private static readonly string BasePath = Environment.CurrentDirectory + "/Userdata/";
+
+ internal static string Expand(string path)
+ {
+ return path.Replace(Metavariable, BasePath);
+ }
+}
diff --git a/rowsSharp/Domain/Cell.cs b/rowsSharp/Domain/Cell.cs
new file mode 100644
index 0000000..3e36ace
--- /dev/null
+++ b/rowsSharp/Domain/Cell.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Controls;
+
+namespace rowsSharp.Domain;
+
+internal static class Cell
+{
+ private static IEnumerable WhereValid(this IList cells)
+ {
+ return cells.Where(x => x.IsValid);
+ }
+
+ // Rows
+ internal static IEnumerable> Rows(this IList cells)
+ {
+ return cells.WhereValid()
+ .Select(x => (ObservableCollection)x.Item)
+ .Distinct();
+ }
+
+ internal static IEnumerable RowIndices(this IList cells, IList> records)
+ {
+ return cells.Rows()
+ .Select(x => records.IndexOf(x));
+ }
+
+ // Columns
+ internal static IEnumerable Columns(this IList cells)
+ {
+ return cells.WhereValid()
+ .Select(x => (string)x.Column.Header)
+ .Distinct();
+ }
+
+ internal static IEnumerable ColumnIndices(this IList cells, IList headers)
+ {
+ return cells.Columns()
+ .Select(x => headers.IndexOf(x));
+ }
+
+ // Cell
+ internal static int RowIndex(this DataGridCellInfo cell, IList> records)
+ {
+ return records.IndexOf((ObservableCollection)cell.Item);
+ }
+
+ internal static int ColumnIndex(this DataGridCellInfo cell, IList headers)
+ {
+ return headers.IndexOf((string)cell.Column.Header);
+ }
+}
diff --git a/rowsSharp/Domain/ColumnNotation.cs b/rowsSharp/Domain/ColumnNotation.cs
new file mode 100644
index 0000000..4dd70eb
--- /dev/null
+++ b/rowsSharp/Domain/ColumnNotation.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace rowsSharp.Domain;
+
+internal static class ColumnNotation
+{
+ internal static string Expand(string path, IList headers, IList row)
+ {
+ foreach (var (header, field) in headers.Zip(row))
+ {
+ path = path.Replace("<" + header + ">", field);
+ }
+ return path;
+ }
+}
diff --git a/rowsSharp/Domain/ColumnStyleHelper.cs b/rowsSharp/Domain/ColumnStyleHelper.cs
new file mode 100644
index 0000000..44fb906
--- /dev/null
+++ b/rowsSharp/Domain/ColumnStyleHelper.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace rowsSharp.Domain;
+
+internal static class ColumnStyleHelper
+{
+ private static Style GetDefaultStyle(Type type)
+ {
+ return new(
+ type,
+ (Style)Application.Current.FindResource(type)
+ );
+ }
+
+
+ internal static Style GetConditionalFormatting(this Dictionary> colorStyles, string header, int column)
+ {
+ Style style = GetDefaultStyle(typeof(DataGridCell));
+
+ colorStyles.TryGetValue(header, out var rules);
+ if (rules is null) { return style; }
+
+ var triggers = GetDataTriggers(column, rules);
+ style.Triggers.AddRange(triggers);
+
+ return style;
+ }
+
+ private static IEnumerable GetDataTriggers(int column, IDictionary rules)
+ {
+ foreach (var (key, value) in rules)
+ {
+ yield return GetDataTrigger(column, key, value);
+ }
+ }
+
+ private static DataTrigger GetDataTrigger(int column, string key, string color)
+ {
+ DataTrigger dataTrigger = new()
+ {
+ Binding = new Binding("[" + column + "]"),
+ Value = key
+ };
+
+ dataTrigger.Setters.Add(new Setter()
+ {
+ Property = Control.BackgroundProperty,
+ Value = new BrushConverter().ConvertFrom(color)
+ });
+
+ return dataTrigger;
+ }
+
+ private static Style? editingElementStyle;
+ private static Style SetEditingElementStyle(bool allowMultiline)
+ {
+ Style style = GetDefaultStyle(typeof(TextBoxBase));
+
+ List setters = new()
+ {
+ new(TextBoxBase.AcceptsReturnProperty, allowMultiline),
+ new(TextBoxBase.PaddingProperty, new Thickness(1, 1, 0, 0)),
+ new(TextBoxBase.MarginProperty, new Thickness(-2))
+ };
+
+ style.Setters.AddRange(setters);
+ return style;
+ }
+
+ internal static Style GetEditingElementStyle(bool allowMultiline)
+ => editingElementStyle ??= SetEditingElementStyle(allowMultiline);
+}
diff --git a/rowsSharp/Domain/CsvFile.cs b/rowsSharp/Domain/CsvFile.cs
new file mode 100644
index 0000000..4258c6f
--- /dev/null
+++ b/rowsSharp/Domain/CsvFile.cs
@@ -0,0 +1,39 @@
+using Microsoft.Win32;
+using ObservableTable.Core;
+using ObservableTable.IO;
+using System.IO;
+
+namespace rowsSharp.Domain;
+
+internal class CsvFile
+{
+ internal static ObservableTable Import(string path, bool hasHeader)
+ {
+ if (!File.Exists(path)) { return new(); }
+
+ return Importer.FromFilePath(path, hasHeader);
+ }
+
+ private static string RequestFilePath()
+ {
+ SaveFileDialog dialog = new()
+ {
+ Filter = "Comma-seperated values (*.csv)|*.csv|All files (*.*)|*.*",
+ DefaultExt = "csv"
+ };
+
+ dialog.ShowDialog();
+
+ return dialog.FileName;
+ }
+
+ internal static void Export(string path, ObservableTable table, bool hasHeader)
+ {
+ if (!File.Exists(path))
+ {
+ path = RequestFilePath();
+ }
+
+ Exporter.ToFile(path, table, hasHeader);
+ }
+}
diff --git a/rowsSharp/Domain/Filter.cs b/rowsSharp/Domain/Filter.cs
new file mode 100644
index 0000000..6d46ea5
--- /dev/null
+++ b/rowsSharp/Domain/Filter.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Windows.Data;
+
+namespace rowsSharp.Domain;
+
+internal class Filter
+{
+ private static readonly Regex splitBySpace = new(@"\s + (?= (?:\""[^\""] *\"" |[^\""]) *$)");
+ private static readonly Regex splitByColon = new(@"(?> filter = new();
+
+ internal Dictionary> Alias { get; set; } = new();
+ internal IList Headers { get; set; } = new List();
+ internal string FilterText { get; set; } = "";
+ internal ICollectionView CollectionView { get; private set; }
+ private ICollectionView? originalCollectionView;
+
+ internal bool UseRegex { get; set; }
+ internal bool UseInputAlias { get; set; }
+ private bool useOutputAlias;
+ internal bool UseOutputAlias
+ {
+ get => useOutputAlias;
+ set
+ {
+ useOutputAlias = value;
+ if (value)
+ {
+ if (originalCollectionView != CollectionView)
+ {
+ originalCollectionView = CollectionView;
+ }
+ return;
+ }
+ if (originalCollectionView is null) { return; }
+ CollectionView = originalCollectionView;
+ }
+ }
+
+ internal Filter(ICollectionView collectionView)
+ {
+ CollectionView = collectionView;
+ }
+
+ internal ICollectionView Invoke()
+ {
+ try
+ {
+ SplitInput();
+ if (UseRegex)
+ {
+ ValidateRegex();
+ }
+ }
+ catch
+ {
+ CollectionView.Filter = (object obj) => false;
+ return CollectionView;
+ }
+
+ CollectionView.Filter = Predicate;
+
+ return UseOutputAlias ? OutputAlias() : CollectionView;
+ }
+
+ private string InputAlias(string header, string value)
+ {
+ if (!Alias.TryGetValue(header, out var columnAlias)) { return value; }
+
+ foreach ((string raw, string aliased) in columnAlias)
+ {
+ if (string.IsNullOrEmpty(aliased)) { continue; }
+ value = value.Replace(aliased, raw);
+ }
+ return value;
+ }
+
+ private static string? OutputAlias(string? value, IDictionary alias)
+ {
+ foreach ((string raw, string aliased) in alias)
+ {
+ if (string.IsNullOrEmpty(raw)) { continue; }
+ value = value?.Replace(raw, aliased);
+ }
+ return value;
+ }
+
+ private ICollectionView OutputAlias()
+ {
+ // Deep copy CollectionView
+ var tempRecords = CollectionView.Cast();
+
+ // Apply alias per COLUMN
+ foreach (var (header, alias) in Alias)
+ {
+ int index = Headers.IndexOf(header);
+ foreach (var record in tempRecords)
+ {
+ record[index] = OutputAlias(record[index], alias);
+ }
+ }
+ return CollectionViewSource.GetDefaultView(tempRecords);
+ }
+
+ private void SplitInput()
+ {
+ filter.Clear();
+ if (string.IsNullOrEmpty(FilterText)) { return; }
+
+ var filters = splitBySpace.Split(FilterText);
+
+ foreach (string criterion in filters)
+ {
+ string[] keyvalue = splitByColon.Split(criterion.Replace("\"", ""));
+
+ // Value only
+ if (keyvalue.Length == 1)
+ {
+ ParseValue(keyvalue[0]);
+ continue;
+ }
+
+ // Key:Value
+ ParseKeyValue(keyvalue);
+ }
+ }
+
+ private void ParseValue(string criterion)
+ {
+ filter.Add(new(-1, criterion));
+ }
+
+ private void ParseKeyValue(string[] criterion)
+ {
+ int column = Headers.IndexOf(criterion[0]);
+
+ if (column == -1)
+ {
+ throw new IndexOutOfRangeException($"Invalid column {criterion[0]}");
+ }
+
+ if (UseInputAlias)
+ {
+ criterion[1] = InputAlias(criterion[0], criterion[1]);
+ }
+
+ filter.Add(new(column, criterion[1]));
+ }
+
+ private void ValidateRegex()
+ {
+ foreach ((int column, string criterion) in filter)
+ {
+ if (column == -1)
+ {
+ continue;
+ }
+
+ // Throws exception on invalid regex
+ Regex.IsMatch("", criterion);
+ }
+ }
+
+ private static string ToCsvString(IList row)
+ {
+ return '"' + string.Join("\",\"", row) + '"';
+ }
+
+ private bool Predicate(object obj)
+ {
+ var row = (IList)obj;
+ foreach ((int column, string pattern) in filter)
+ {
+ string input = (column == -1 ? ToCsvString(row) : row[column]) ?? "";
+
+ if (
+ (UseRegex && Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase))
+ || (!UseRegex && input.Contains(pattern, StringComparison.InvariantCultureIgnoreCase))
+ )
+ { continue; }
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/rowsSharp/Domain/PreferencesReader.cs b/rowsSharp/Domain/PreferencesReader.cs
new file mode 100644
index 0000000..d239987
--- /dev/null
+++ b/rowsSharp/Domain/PreferencesReader.cs
@@ -0,0 +1,69 @@
+using rowsSharp.Model;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Windows;
+using System.Windows.Markup;
+
+namespace rowsSharp.Domain;
+
+internal static class PreferencesReader
+{
+ internal static Preferences Import(string path)
+ {
+ App.Logger.Info("Reading preferences");
+
+ Preferences config = FromPath(path);
+ config.ExpandBaseDir();
+ config.ApplyTheming();
+ return config;
+ }
+
+ private static Preferences FromPath(string path)
+ {
+ try
+ {
+ string json = File.ReadAllText(path);
+ return JsonSerializer.Deserialize(json) ?? new();
+ }
+ catch (Exception ex)
+ {
+ App.Logger.Fatal(ex, "Error reading preferences");
+ throw;
+ }
+ }
+
+ private static void ExpandBaseDir(this Preferences config)
+ {
+ config.CsvPath = BaseDir.Expand(config.CsvPath);
+ config.PreviewPath = BaseDir.Expand(config.PreviewPath);
+ config.StylePath = BaseDir.Expand(config.StylePath);
+ config.ThemePath = BaseDir.Expand(config.ThemePath);
+ }
+
+ private static void ApplyTheming(this Preferences config)
+ {
+ config.ColumnStyle = GetColumnStyle(config.StylePath);
+ config.Theme = GetTheme(config.ThemePath);
+ }
+
+ private static ColumnStyle GetColumnStyle(string path)
+ {
+ if (!File.Exists(path)) { return new(); }
+
+ App.Logger.Info("Parsing conditional formatting configurations");
+
+ string json = File.ReadAllText(path);
+ return JsonSerializer.Deserialize(json) ?? new();
+ }
+
+ private static ResourceDictionary GetTheme(string path)
+ {
+ if (!File.Exists(path)) { return new(); }
+
+ App.Logger.Info("Parsing XAML theme file");
+
+ string xaml = File.ReadAllText(path);
+ return (ResourceDictionary)XamlReader.Parse(xaml);
+ }
+}
diff --git a/rowsSharp/Domain/Preview.cs b/rowsSharp/Domain/Preview.cs
new file mode 100644
index 0000000..a6b6131
--- /dev/null
+++ b/rowsSharp/Domain/Preview.cs
@@ -0,0 +1,19 @@
+using System.IO;
+using System.Windows.Media.Imaging;
+
+namespace rowsSharp.Domain;
+
+internal static class Preview
+{
+ internal static BitmapImage? FromPath(string path)
+ {
+ if (!File.Exists(path)) { return null; }
+
+ BitmapImage image = new();
+ image.BeginInit();
+ image.CacheOption = BitmapCacheOption.OnLoad;
+ image.UriSource = new(path);
+ image.EndInit();
+ return image;
+ }
+}
diff --git a/rowsSharp/Domain/RowTemplate.cs b/rowsSharp/Domain/RowTemplate.cs
new file mode 100644
index 0000000..4f5bc5f
--- /dev/null
+++ b/rowsSharp/Domain/RowTemplate.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace rowsSharp.Domain;
+
+internal static class RowTemplate
+{
+ internal static IEnumerable Generate(int count, IList headers, IDictionary? template)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var row = new string?[headers.Count];
+
+ if (template is not null)
+ {
+ row = ApplyTemplate(row, i, count, headers, template);
+ }
+
+ yield return row;
+ }
+ }
+
+ private static string?[] ApplyTemplate(string?[] row, int rowIndex, int count, IList headers, IDictionary template)
+ {
+ foreach (var (column, value) in template)
+ {
+ int index = headers.IndexOf(column);
+ if (index == -1) { continue; }
+
+ row[index] = Expand(value, rowIndex, count);
+ }
+ return row;
+ }
+
+ private static string ExpandOriginalNotation(string value)
+ {
+ // These will be deprecated soon.
+ return value.Replace("", "")
+ .Replace("", "")
+ .Replace("", "")
+ .Replace("", "");
+ }
+
+ private readonly static Regex column = new("(?<=<)(.+?)(?=>)");
+
+ private static string ExpandDateTime(string value)
+ {
+ var matches = column.Matches(value).OfType();
+ var now = DateTime.Now;
+
+ foreach (var match in matches)
+ {
+ try
+ {
+ value = value.Replace("<" + match + ">", now.ToString(match.Value));
+ }
+ catch (FormatException) { }
+ }
+
+ return value;
+ }
+
+ private static string ExpandIndexer(string value, int rowIndex, int count)
+ {
+ return value.Replace("<#>", rowIndex.ToString())
+ .Replace("", (count - rowIndex - 1).ToString());
+ }
+
+ private static string? Expand(string? value, int rowIndex, int count)
+ {
+ if (value is null) { return null; }
+
+ value = ExpandIndexer(value, rowIndex, count);
+ value = ExpandOriginalNotation(value);
+ value = ExpandDateTime(value);
+
+ return value;
+ }
+}
diff --git a/rowsSharp/Domain/UniqueColumn.cs b/rowsSharp/Domain/UniqueColumn.cs
new file mode 100644
index 0000000..a714b19
--- /dev/null
+++ b/rowsSharp/Domain/UniqueColumn.cs
@@ -0,0 +1,22 @@
+using ObservableTable.Core;
+using System.Collections.Generic;
+
+namespace rowsSharp.Domain;
+
+internal class UniqueColumn
+{
+ private int index;
+
+ private static Column GetNumberedColumn(int index)
+ {
+ return new(index.ToString());
+ }
+
+ internal IEnumerable> Next(int count = 1)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ yield return GetNumberedColumn(index++);
+ }
+ }
+}
diff --git a/rowsSharp/Internal/INPC.cs b/rowsSharp/INotifyPropertyChanged.cs
similarity index 75%
rename from rowsSharp/Internal/INPC.cs
rename to rowsSharp/INotifyPropertyChanged.cs
index 6268ca3..0771dcf 100644
--- a/rowsSharp/Internal/INPC.cs
+++ b/rowsSharp/INotifyPropertyChanged.cs
@@ -3,12 +3,12 @@
namespace rowsSharp;
-public abstract class INPC : INotifyPropertyChanged
+public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ PropertyChanged?.Invoke(this, new(propertyName));
}
private protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = "")
diff --git a/rowsSharp/Internal/DataContext.cs b/rowsSharp/Internal/DataContext.cs
deleted file mode 100644
index c5fde34..0000000
--- a/rowsSharp/Internal/DataContext.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using rowsSharp.DataStore;
-using rowsSharp.Domain;
-using rowsSharp.ViewModel;
-using System.ComponentModel;
-using System.Linq;
-using System.Windows;
-
-namespace rowsSharp;
-
-internal class DataContext : INPC
-{
- // DataStore
- public OperationHistory OperationHistory { get; } = new();
- public Config Config { get; }
- public Csv Csv { get; }
-
- // ViewModel
- public Command Command { get; }
- public RecordsView RecordsView { get; }
- public Status Status { get; } = new();
-
- // Domain
- internal Filter Filter { get; }
- internal Preview Preview { get; }
- internal Edit Edit { get; }
- internal History History { get; }
-
- internal DataContext()
- {
- Config = Domain.IO.Config.Import();
-
- Csv = Domain.IO.Csv.Import(Config.CsvPath, Config.HasHeader);
- RecordsView = new(Csv.Records);
-
- Filter = new(Status, Config, Csv, RecordsView);
- Preview = new(Status, Csv, Config.PreviewPath, Config.CopyRowFormat);
- History = new(Status, OperationHistory, Csv);
- Edit = new(Status, Config, Csv, History);
- Command = new(this);
-
- if (!Csv.Records.Any())
- {
- App.Logger.Warn("CSV file not found. Starting creation wizard.");
- _ = new InitWizardWindow(Config);
-
- Csv = Domain.IO.Csv.Import(Config.CsvPath, Config.HasHeader);
- RecordsView = new(Csv.Records);
- }
- }
-
- public DelegateCommand Exit => new(
- (e) =>
- {
- App.Logger.Info("Changes unsaved, asking for confirmation before exiting.");
- MessageBoxResult dialog = MessageBox.Show(
- "Save changes before exiting?",
- "RowsSharp",
- MessageBoxButton.YesNoCancel,
- MessageBoxImage.Question
- );
-
- if (dialog == MessageBoxResult.Cancel)
- {
- e.Cancel = true;
- }
- else if (dialog == MessageBoxResult.Yes)
- {
- Edit.Save();
- }
- },
- (e) => Status.IsDirtyEditor
- );
-}
diff --git a/rowsSharp/Internal/DataStore/Store.Config.cs b/rowsSharp/Internal/DataStore/Store.Config.cs
deleted file mode 100644
index bd0e7e3..0000000
--- a/rowsSharp/Internal/DataStore/Store.Config.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using rowsSharp.Model;
-
-namespace rowsSharp.DataStore;
-
-public class Config : INPC
-{
- public string CsvPath { get; set; } = "$baseDir/CSVData/data.csv";
- public bool HasHeader { get; init; } = true;
- public string StylePath { get; set; } = string.Empty;
- public string PreviewPath { get; set; } = string.Empty;
- public string CopyRowFormat { get; init; } = string.Empty;
-
- public bool UseInputAlias { get; set; }
-
- private bool useOutputAlias;
- public bool UseOutputAlias
- {
- get => useOutputAlias;
- set => SetField(ref useOutputAlias, value);
- }
-
- public bool UseRegexFilter { get; init; }
- public bool UseToolTip { get; init; } = true;
-
- private bool canEdit;
- public bool CanEdit
- {
- get => canEdit;
- set => SetField(ref canEdit, value);
- }
-
- internal bool OriginalCanEdit { get; set; } = true;
- public bool AllowMultiline { get; set; } = true;
-
- private bool insertSelectedCount;
- public bool InsertSelectedCount
- {
- get => insertSelectedCount;
- set => SetField(ref insertSelectedCount, value);
- }
-
- public int InsertCount { get; init; }
- public bool UseInsertTemplate { get; set; }
- public string ThemePath { get; set; } = "$baseDir/Configurations/Themes/Light.xaml";
- public ColumnStyle Style { get; set; }
-}
diff --git a/rowsSharp/Internal/DataStore/Store.Csv.cs b/rowsSharp/Internal/DataStore/Store.Csv.cs
deleted file mode 100644
index c723293..0000000
--- a/rowsSharp/Internal/DataStore/Store.Csv.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using rowsSharp.Model;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-
-namespace rowsSharp.DataStore;
-
-internal class Csv : INPC
-{
- internal List Headers = new();
- internal ObservableCollection Records = new();
-}
diff --git a/rowsSharp/Internal/DataStore/Store.OperationHistory.cs b/rowsSharp/Internal/DataStore/Store.OperationHistory.cs
deleted file mode 100644
index 1607856..0000000
--- a/rowsSharp/Internal/DataStore/Store.OperationHistory.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using rowsSharp.Model;
-using System.Collections.Generic;
-
-namespace rowsSharp.DataStore;
-
-internal class OperationHistory
-{
- internal Stack UndoStack = new();
- internal Stack RedoStack = new();
-}
diff --git a/rowsSharp/Internal/Domain/Domain.Edit.cs b/rowsSharp/Internal/Domain/Domain.Edit.cs
deleted file mode 100644
index 8d2274e..0000000
--- a/rowsSharp/Internal/Domain/Domain.Edit.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-using rowsSharp.Model;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Windows.Controls;
-
-namespace rowsSharp.Domain;
-internal class Edit
-{
- private readonly ViewModel.Status status;
- private readonly DataStore.Config config;
- private readonly DataStore.Csv csv;
- private readonly History history;
-
- internal Edit (ViewModel.Status status, DataStore.Config config, DataStore.Csv csv, History history)
- {
- this.status = status;
- this.config = config;
- this.csv = csv;
- this.history = history;
- }
-
- internal void OutputAliasEditing()
- {
- // Make ReadWrite FALSE when OutputAlias is TRUE
- // Revert OriginalCanEdit when OutputAlias is FALSE
- if (config.UseOutputAlias) { config.OriginalCanEdit = config.CanEdit; }
- config.CanEdit = !config.UseOutputAlias && config.OriginalCanEdit;
- }
-
- internal void BeginEdit() { status.IsEditing = true; }
-
- internal void EndEdit(DataGridCellEditEndingEventArgs e)
- {
- var record = (Record)e.Row.Item;
- int columnIndex = csv.Headers.IndexOf(e.Column.Header.ToString()!);
- string oldString = record.GetField(columnIndex);
-
- if (((TextBox)e.EditingElement).Text == oldString) { return; }
-
- history.AddOperation(
- OperationType.Inline,
- csv.Records.IndexOf(record),
- record.DeepCopy(csv.Headers.Count)
- );
- history.CommitOperation();
- }
-
- internal bool CanInsertTopOrBottom() =>
- config.CanEdit &&
- (
- (config.InsertSelectedCount && status.SelectedItems.Any()) ||
- (!config.InsertSelectedCount)
- );
-
- internal bool IsAnyRowSelected() =>
- config.CanEdit && status.SelectedIndex != -1;
-
- internal void Remove()
- {
- App.Logger.Info("Removing rows (x{Count})", status.SelectedItems.Count);
- foreach (Record item in status.SelectedItems)
- {
- history.AddOperation(
- OperationType.Remove,
- csv.Records.IndexOf(item),
- item
- );
- csv.Records.Remove(item);
- }
- history.CommitOperation();
- }
-
- internal void Insert(int at)
- {
- if (at == -1) { at = csv.Records.Count; }
-
- int count = config.InsertSelectedCount
- ? status.SelectedItems.Count
- : config.InsertCount;
-
- App.Logger.Info("Inserting CSV (@{At} x{Count}, Template: {Template})", at, count, config.UseInsertTemplate);
- status.IsInsertExpanded = false;
-
- DateTime now = DateTime.Now;
- Record templatedRow = new();
-
- // Templating. Expand static <[DdTt]> fields beforehand.
- if (config.UseInsertTemplate)
- {
- foreach (KeyValuePair keyValuePair in config.Style.Template)
- {
- int columnIndex = csv.Headers.IndexOf(keyValuePair.Key);
- templatedRow.SetField(
- columnIndex,
- keyValuePair.Value
- .Replace("", now.ToString("yyyyMMdd"))
- .Replace("", now.ToString("yyyy-MM-dd"))
- .Replace("", now.ToString("HHmmss"))
- .Replace("", now.ToString("HH:mm:ss"))
- );
- }
- }
-
- // Insert
- for (int i = 0; i < count; i++)
- {
- Record thisRow = templatedRow.DeepCopy(csv.Headers.Count);
- for (int j = 0; j < csv.Headers.Count; j++)
- {
- thisRow.SetField(
- j,
- thisRow.GetField(j)
- .Replace("<#>", i.ToString())
- .Replace("", (count - i - 1).ToString())
- );
- }
-
- csv.Records.Insert(at + i, thisRow);
- history.AddOperation(OperationType.Insert, at + i, thisRow);
- }
- history.CommitOperation();
- status.ScrollAfterInsert = true;
- status.SelectedIndex = at;
- }
-
- internal void Save()
- {
- App.Logger.Info("Saving");
- using StreamWriter writer = new(config.CsvPath);
-
- string fullHeader = string.Join(
- ",",
- csv.Headers.Select(m => "\"" + m.Replace("\"", "\"\"") + "\"")
- );
- writer.WriteLine(fullHeader);
-
- foreach (Record record in csv.Records)
- {
- string toOutput = record.ConcatenateFields(csv.Headers.Count);
- writer.WriteLine(toOutput);
- }
-
- status.IsDirtyEditor = false;
- }
-}
diff --git a/rowsSharp/Internal/Domain/Domain.Filter.cs b/rowsSharp/Internal/Domain/Domain.Filter.cs
deleted file mode 100644
index b7e513c..0000000
--- a/rowsSharp/Internal/Domain/Domain.Filter.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using rowsSharp.Model;
-using rowsSharp.ViewModel;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Reflection;
-using System.Text.RegularExpressions;
-using System.Windows.Data;
-
-namespace rowsSharp.Domain;
-
-internal class Filter
-{
- private readonly Status status;
- private readonly DataStore.Config config;
- private readonly DataStore.Csv csv;
- private readonly RecordsView recordsView;
-
- internal Filter(Status inputStatus, DataStore.Config inputConfig, DataStore.Csv inputCsv, RecordsView inputRecordsView)
- {
- status = inputStatus;
- config = inputConfig;
- csv = inputCsv;
- recordsView = inputRecordsView;
- }
-
- internal void FocusFilter()
- {
- // Force update ViewModel by refocusing
- status.IsFilterFocused = false;
- status.IsFilterFocused = true;
- }
-
- private List> criteria = new();
-
- private List> ParseInput()
- {
- List> output = new();
-
- string[] splitFilterText = Regex.Split(status.FilterText, "\\s+(?=(?:\"[^\"]*\"|[^\"])*$)");
- foreach (string criterion in splitFilterText)
- {
- string[] keyvalue = Regex.Split(criterion, ":(?=(?:\"[^\"]*\"|[^\"])*$)");
- // Handle value-only criterion (default)
- int column = -1;
- string value = keyvalue[^1].Trim('"');
-
- // Extra handling for Key:Value
- if (keyvalue.Length == 2)
- {
- string header = keyvalue[0].Trim('"');
- column = csv.Headers.IndexOf(header);
-
- if (column == -1)
- {
- throw new InvalidFilterCriteriaException($"Invalid column {header}");
- }
-
- // Input alias
- value = keyvalue[1].Trim('"');
- if (config.UseInputAlias)
- {
- Dictionary columnAlias = config.Style.Alias.GetValueOrDefault(header, new());
- foreach ((string raw, string aliased) in columnAlias)
- {
- value = value.Replace(aliased, raw);
- }
- }
- }
-
- // Validate regular expression
- if (config.UseRegexFilter)
- {
- try
- {
- Regex.IsMatch("", value);
- }
- catch
- {
- throw new InvalidFilterCriteriaException($"Invalid regex {value}");
- }
- }
- output.Add(new(column, value));
- }
- return output;
- }
-
- private ICollectionView OutputAlias()
- {
- List tempRecords = new();
- foreach (Record record in recordsView.CollectionView)
- {
- Record thisRecord = record.DeepCopy(csv.Headers.Count);
- for (int i = 0; i < csv.Headers.Count; i++)
- {
- Dictionary thisAlias = config.Style.Alias.GetValueOrDefault(csv.Headers[i], new());
- foreach ((string raw, string aliased) in thisAlias)
- {
- thisRecord.SetField(
- i,
- thisRecord.GetField(i).Replace(raw, aliased)
- );
- }
- }
- tempRecords.Add(thisRecord);
- }
- return CollectionViewSource.GetDefaultView(tempRecords);
- }
-
- private bool RecordsViewFilter(object obj)
- {
- var row = (Record)obj;
- foreach ((int column, string pattern) in criteria)
- {
- string input = column == -1
- ? row.ConcatenateFields(csv.Headers.Count)
- : row.GetField(column);
-
- if (
- (config.UseRegexFilter && Regex.IsMatch(input.ToLower(), pattern))
- || (!config.UseRegexFilter && input.ToLower().Contains(pattern.ToLower()))
- )
- { continue; }
- return false;
- }
- return true;
- }
-
- internal void DoFilter()
- {
- App.Logger.Info("Filtering CSV, ({filter}, IOAlias: {IAlias}, {OAlias})",
- status.FilterText,
- config.UseInputAlias,
- config.UseOutputAlias
- );
-
- // Parse input
- try
- {
- criteria = ParseInput();
- }
- catch (InvalidFilterCriteriaException ex)
- {
- App.Logger.Warn(ex.Message);
- recordsView.CollectionView.Filter = (object obj) => false;
- return;
- }
-
- // Filtering
- recordsView.CollectionView = CollectionViewSource.GetDefaultView(csv.Records);
- recordsView.CollectionView.Filter = RecordsViewFilter;
-
- // Output alias
- if (config.UseOutputAlias)
- {
- recordsView.CollectionView = OutputAlias();
- }
-
- criteria = new();
- status.PreviewBitmap = new();
- status.SelectedIndex = -1;
- App.Logger.Debug("Filtering CSV completed");
- }
-}
diff --git a/rowsSharp/Internal/Domain/Domain.History.cs b/rowsSharp/Internal/Domain/Domain.History.cs
deleted file mode 100644
index 9020e5c..0000000
--- a/rowsSharp/Internal/Domain/Domain.History.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using rowsSharp.Model;
-using System;
-using System.Windows;
-
-namespace rowsSharp.Domain;
-internal class History
-{
- private readonly ViewModel.Status status;
- private readonly DataStore.OperationHistory operation;
- private readonly DataStore.Csv csv;
- internal History(ViewModel.Status status, DataStore.OperationHistory operation, DataStore.Csv csv)
- {
- this.status = status;
- this.operation = operation;
- this.csv = csv;
- }
-
- private bool parity;
- public void AddOperation(OperationType operationType, int at, Record oldRow)
- {
- operation.UndoStack.Push(new Operation(operationType, at, oldRow, parity));
- }
-
- public void CommitOperation()
- {
- operation.RedoStack.Clear();
- status.IsDirtyEditor = true;
- parity = !parity;
- }
-
- private static void DispatcherInvoke(Action action)
- {
- Application.Current.Dispatcher.BeginInvoke(action);
- }
-
- private void CommonOperation(bool isUndo, Operation last)
- {
- OperationType action = last.OperationType;
- if (action == OperationType.Inline)
- {
- DispatcherInvoke(() => csv.Records[last.At] = last.OldRow);
- }
-
- else if ((isUndo && action == OperationType.Remove) ||
- (!isUndo && action == OperationType.Insert)
- )
- {
- DispatcherInvoke(() => csv.Records.Insert(last.At, last.OldRow));
- }
- else
- {
- DispatcherInvoke(() => csv.Records.RemoveAt(last.At));
- }
- }
-
- internal void Undo()
- {
- Operation last = operation.UndoStack.Pop();
- App.Logger.Info("Undo {Action} @ {At}, {Parity}", last.OperationType, last.At, last.Parity);
-
- operation.RedoStack.Push(new Operation(
- last.OperationType,
- last.At,
- last.OperationType == OperationType.Inline ? csv.Records[last.At] : last.OldRow,
- last.Parity
- ));
- CommonOperation(true, last);
-
- // Group insert/remove edits
- operation.UndoStack.TryPeek(out Operation? next);
- if (last.Parity == next?.Parity) { Undo(); }
- }
-
- internal void Redo()
- {
- Operation last = operation.RedoStack.Pop();
- App.Logger.Info("Redo {Action} @ {At}, {Parity}", last.OperationType, last.At, last.Parity);
-
- operation.UndoStack.Push(new Operation(
- last.OperationType,
- last.At,
- last.OperationType == OperationType.Inline ? csv.Records[last.At] : last.OldRow,
- last.Parity
- ));
- CommonOperation(false, last);
-
- operation.RedoStack.TryPeek(out Operation? next);
- if (last.Parity == next?.Parity) { Redo(); }
- }
-}
diff --git a/rowsSharp/Internal/Domain/Domain.IO.Config.cs b/rowsSharp/Internal/Domain/Domain.IO.Config.cs
deleted file mode 100644
index deed83f..0000000
--- a/rowsSharp/Internal/Domain/Domain.IO.Config.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.IO;
-using System.Text.Json;
-using System.Windows;
-using System.Windows.Markup;
-
-namespace rowsSharp.Domain.IO;
-
-internal static class Config
-{
- private const string ConfigPath = "./Userdata/Configurations/Configuration.json";
-
- internal static DataStore.Config Import(string path = ConfigPath)
- {
- // General configuration
- if (!File.Exists(path))
- {
- FileNotFoundException ex = new(path);
- App.Logger.Fatal(ex, "Base configuration file not found.");
- throw ex;
- }
-
- App.Logger.Info("Loading base configurations");
- string jsonString = File.ReadAllText(path);
- DataStore.Config config = JsonSerializer.Deserialize(jsonString) ?? new();
-
- config.OriginalCanEdit = config.CanEdit;
-
- string baseDir = Environment.CurrentDirectory + "./Userdata/";
- config.CsvPath = config.CsvPath.Replace("$baseDir", baseDir);
- config.StylePath = config.StylePath.Replace("$baseDir", baseDir);
- config.PreviewPath = config.PreviewPath.Replace("$baseDir", baseDir);
- config.ThemePath = config.ThemePath.Replace("$baseDir", baseDir);
-
- // Conditional Formatting
- if (File.Exists(config.StylePath))
- {
- App.Logger.Info("Loading optional conditional formatting configurations");
- jsonString = File.ReadAllText(config.StylePath);
- config.Style = JsonSerializer.Deserialize(jsonString);
- }
-
- // Themeing
- if (File.Exists(config.ThemePath))
- {
- App.Logger.Info("Loading XAML theme file");
- using StreamReader streamReader = new(config.ThemePath);
- try
- {
- var dictionary = (ResourceDictionary)XamlReader.Load(streamReader.BaseStream);
- Application.Current.Resources.MergedDictionaries.Add(dictionary);
- }
- catch (XamlParseException e)
- {
- App.Logger.Error(e, "Error parsing XAML theme file");
- }
- }
- return config;
- }
-}
diff --git a/rowsSharp/Internal/Domain/Domain.IO.Csv.cs b/rowsSharp/Internal/Domain/Domain.IO.Csv.cs
deleted file mode 100644
index 9b78fdb..0000000
--- a/rowsSharp/Internal/Domain/Domain.IO.Csv.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using CsvHelper;
-using CsvHelper.Configuration;
-using rowsSharp.Model;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-
-namespace rowsSharp.Domain.IO;
-
-internal static class Csv
-{
- internal static DataStore.Csv Import(string path, bool hasHeader)
- {
- if (!File.Exists(path)) { return new(); }
-
- CsvConfiguration config = new(CultureInfo.InvariantCulture)
- {
- MissingFieldFound = null,
- BadDataFound = null,
- HasHeaderRecord = hasHeader
- };
-
- App.Logger.Info("Loading CSV @ {path}", path);
- using StreamReader reader = new(path);
- using CsvReader csvReader = new(reader, config);
- csvReader.Context.RegisterClassMap();
-
- DataStore.Csv csv = new()
- {
- Records = new(csvReader.GetRecords()),
- Headers = csvReader.Context.Reader.HeaderRecord?.ToList() ?? new()
- };
-
- // Default headers
- if (csv.Headers.Any() || !csv.Records.Any()) { return csv; }
-
- for (int i = 0; i < RecordMap.MaxColumns - 1; i++)
- {
- if (csv.Records[0].GetField(i) == string.Empty) { break; }
- csv.Headers.Add("Column" + i);
- }
- return csv;
- }
-}
diff --git a/rowsSharp/Internal/Domain/Domain.InitWizard.cs b/rowsSharp/Internal/Domain/Domain.InitWizard.cs
deleted file mode 100644
index 8bf5369..0000000
--- a/rowsSharp/Internal/Domain/Domain.InitWizard.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.IO;
-using System.Text.RegularExpressions;
-
-namespace rowsSharp.Domain;
-
-internal class InitWizard : INPC
-{
- private DataStore.Config config;
- public DataStore.Config Config
- {
- get => config;
- set => SetField(ref config, value);
- }
-
- internal InitWizard(DataStore.Config config)
- {
- Config = this.config = config;
- }
-
- private string initHeaders = string.Empty;
- public string InitHeaders
- {
- get => initHeaders;
- set => SetField(ref initHeaders, value);
- }
-
- // private DelegateCommand? createCommand;
- public DelegateCommand CreateCommand => new(
- () =>
- {
- string[] toWrite =
- {
- InitHeaders,
- "Placeholder 1"
- };
- File.WriteAllLines(Config.CsvPath, toWrite);
- },
- () => !Regex.IsMatch(InitHeaders, @"^[,\s]*$")
- );
-}
diff --git a/rowsSharp/Internal/Domain/Domain.Preview.cs b/rowsSharp/Internal/Domain/Domain.Preview.cs
deleted file mode 100644
index 8d47587..0000000
--- a/rowsSharp/Internal/Domain/Domain.Preview.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using rowsSharp.Model;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Windows.Media.Imaging;
-using System.Windows;
-using System;
-
-namespace rowsSharp.Domain;
-
-internal class Preview
-{
- private readonly ViewModel.Status status;
- private readonly DataStore.Csv csv;
- private readonly string previewPath;
- private readonly string copyRowFormat;
-
- internal Preview(ViewModel.Status inputStatus, DataStore.Csv inputCsv, string inputPreviewPath, string inputCopyRowFormat)
- {
- status = inputStatus;
- csv = inputCsv;
- previewPath = inputPreviewPath;
- copyRowFormat = inputCopyRowFormat;
- }
-
- private string ExpandColumnNotation(string inString, Record activeRow)
- {
- MatchCollection matches = Regex.Matches(inString, @"(?<=<)(.+?)(?=>)");
- foreach (Match match in matches.Cast())
- {
- int columnIndex = csv.Headers.IndexOf(match.Value);
- if (columnIndex == -1) { return string.Empty; }
-
- string replaceFrom = string.Format("<{0}>", match.Value);
- string replaceTo = activeRow.GetField(columnIndex);
-
- inString = inString.Replace(replaceFrom, replaceTo);
- }
- return inString;
- }
-
- internal void UpdatePreview()
- {
- status.PreviewBitmap = new();
- if (!status.SelectedItems.Any()) { return; }
-
- string path = ExpandColumnNotation(previewPath, status.SelectedItems[0]);
-
- if (!File.Exists(path))
- {
- App.Logger.Warn("Failed to set preview because of non-existent file @ {path}", path);
- status.PreviewBitmap = new();
- return;
- }
-
- // Don't permanently lock the image
- App.Logger.Info("Setting preview to {path}", path);
-
- BitmapImage previewSource = new();
- previewSource.BeginInit();
- previewSource.UriSource = new Uri(path);
- previewSource.CacheOption = BitmapCacheOption.OnLoad;
- previewSource.EndInit();
- previewSource.Freeze();
- status.PreviewBitmap = previewSource;
- }
-
- internal void CopyImage()
- {
- App.Logger.Info("Copying preview image");
- Clipboard.SetImage(status.PreviewBitmap);
- }
-
- internal void CopyString()
- {
- App.Logger.Info("Copying row string");
- string toCopy = ExpandColumnNotation(copyRowFormat, status.SelectedItems[0]);
- Clipboard.SetText(toCopy);
- }
-}
diff --git a/rowsSharp/Internal/Model/Model.Operation.cs b/rowsSharp/Internal/Model/Model.Operation.cs
deleted file mode 100644
index 1a0b4f1..0000000
--- a/rowsSharp/Internal/Model/Model.Operation.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-namespace rowsSharp.Model;
-
-internal enum OperationType
-{
- Inline,
- Insert,
- Remove
-}
-
-internal class Operation
-{
- internal OperationType OperationType { get; set; }
- internal int At { get; set; }
- internal Record OldRow { get; set; } = new();
- internal bool Parity { get; set; }
-
- internal static Operation DeepCopy(Operation copyFrom)
- {
- return new(copyFrom.OperationType, copyFrom.At, copyFrom.OldRow, copyFrom.Parity);
- }
-
- internal Operation(OperationType operationType, int at, Record oldRow, bool parity)
- {
- OperationType = operationType;
- At = at;
- OldRow = oldRow;
- Parity = parity;
- }
-}
diff --git a/rowsSharp/Internal/Model/Model.Record.cs b/rowsSharp/Internal/Model/Model.Record.cs
deleted file mode 100644
index c4d0733..0000000
--- a/rowsSharp/Internal/Model/Model.Record.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using CsvHelper.Configuration;
-
-namespace rowsSharp.Model;
-
-public class Record
-{
- public string Column0 { get; set; } = string.Empty;
- public string Column1 { get; set; } = string.Empty;
- public string Column2 { get; set; } = string.Empty;
- public string Column3 { get; set; } = string.Empty;
- public string Column4 { get; set; } = string.Empty;
- public string Column5 { get; set; } = string.Empty;
- public string Column6 { get; set; } = string.Empty;
- public string Column7 { get; set; } = string.Empty;
- public string Column8 { get; set; } = string.Empty;
- public string Column9 { get; set; } = string.Empty;
- public string Column10 { get; set; } = string.Empty;
- public string Column11 { get; set; } = string.Empty;
- public string Column12 { get; set; } = string.Empty;
- public string Column13 { get; set; } = string.Empty;
- public string Column14 { get; set; } = string.Empty;
- public string Column15 { get; set; } = string.Empty;
- public string Column16 { get; set; } = string.Empty;
- public string Column17 { get; set; } = string.Empty;
- public string Column18 { get; set; } = string.Empty;
- public string Column19 { get; set; } = string.Empty;
- public string Column20 { get; set; } = string.Empty;
- public string Column21 { get; set; } = string.Empty;
- public string Column22 { get; set; } = string.Empty;
- public string Column23 { get; set; } = string.Empty;
- public string Column24 { get; set; } = string.Empty;
- public string Column25 { get; set; } = string.Empty;
- public string Column26 { get; set; } = string.Empty;
- public string Column27 { get; set; } = string.Empty;
- public string Column28 { get; set; } = string.Empty;
- public string Column29 { get; set; } = string.Empty;
- public string Column30 { get; set; } = string.Empty;
- public string Column31 { get; set; } = string.Empty;
-
- internal string GetField(int column)
- {
- if (column < 0 || column > RecordMap.MaxColumns - 1) { return string.Empty; }
-
- // We're absolutely sure that
- // record is not null && "Column" + column is a valid field
- #pragma warning disable CS8602, CS8603
- return GetType()
- .GetProperty("Column" + column)
- .GetValue(this).ToString();
- #pragma warning restore CS8602, CS8603
- }
-
- internal void SetField(int column, string value)
- {
- if (column < 0 || column > RecordMap.MaxColumns - 1) { return; }
- #pragma warning disable CS8602
- GetType()
- .GetProperty("Column" + column)
- .SetValue(this, value);
- #pragma warning restore CS8602
- }
-
- internal Record DeepCopy(int columnCount = RecordMap.MaxColumns - 1)
- {
- Record output = new();
- for (int i = 0; i < columnCount; i++)
- {
- output.SetField(i, GetField(i));
- }
- return output;
- }
-
- internal string ConcatenateFields(int columnCount = RecordMap.MaxColumns - 1)
- {
- string output = string.Empty;
- for (int i = 0; i < columnCount; i++)
- {
- output += '"' + GetField(i).Replace("\"", "\"\"") + "\",";
- }
- return output.TrimEnd(',');
- }
-}
-
-internal class RecordMap : ClassMap
-{
- internal const int MaxColumns = 32;
- internal RecordMap()
- {
- for (int i = 0; i < MaxColumns - 1; i++)
- {
- // Ultra thanks to David Specht on https://stackoverflow.com/a/62601123
- Map(
- typeof(Record),
- typeof(Record).GetProperty("Column" + i))
- .Optional()
- .Index(i);
- }
- }
-}
diff --git a/rowsSharp/Internal/View/Common.xaml b/rowsSharp/Internal/View/Common.xaml
deleted file mode 100644
index 2da5576..0000000
--- a/rowsSharp/Internal/View/Common.xaml
+++ /dev/null
@@ -1,412 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/rowsSharp/Internal/View/InitWizardWindow.xaml b/rowsSharp/Internal/View/InitWizardWindow.xaml
deleted file mode 100644
index 76879f2..0000000
--- a/rowsSharp/Internal/View/InitWizardWindow.xaml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/rowsSharp/Internal/View/InitWizardWindow.xaml.cs b/rowsSharp/Internal/View/InitWizardWindow.xaml.cs
deleted file mode 100644
index 6741240..0000000
--- a/rowsSharp/Internal/View/InitWizardWindow.xaml.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Windows;
-
-namespace rowsSharp;
-
-public partial class InitWizardWindow : Window
-{
- public InitWizardWindow(DataStore.Config config)
- {
- DataContext = new Domain.InitWizard(config);
- InitializeComponent();
- ShowDialog();
- }
-
- // No, not writing a 100-line DepenencyProperty for one line of code.
- private void Button_Click(object sender, RoutedEventArgs e) { Close(); }
-}
diff --git a/rowsSharp/Internal/View/MainWindow.xaml b/rowsSharp/Internal/View/MainWindow.xaml
deleted file mode 100644
index 8931c04..0000000
--- a/rowsSharp/Internal/View/MainWindow.xaml
+++ /dev/null
@@ -1,358 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/rowsSharp/Internal/View/MainWindow.xaml.cs b/rowsSharp/Internal/View/MainWindow.xaml.cs
deleted file mode 100644
index 1b5e60a..0000000
--- a/rowsSharp/Internal/View/MainWindow.xaml.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Media;
-
-namespace rowsSharp;
-
-///
-/// Interaction logic for MainWindow.xaml
-///
-public partial class MainWindow : Window
-{
- // private readonly RowsVM viewModel;
- private readonly DataContext dataContext = new();
-
- public MainWindow()
- {
- DataContext = dataContext;
- InitializeComponent();
- editingStyle = DefineEditingStyle();
- App.Logger.Info("Okay, it's happening! Everybody stay calm!");
- }
-
- // Sorry, no MVVM
- private readonly Style editingStyle;
-
- private Style DefineEditingStyle()
- {
- Style style = new(
- typeof(TextBox),
- (Style)Application.Current.FindResource(typeof(TextBox))
- );
-
- style.Setters.Add(new Setter(System.Windows.Controls.Primitives.TextBoxBase.AcceptsReturnProperty, dataContext.Config.AllowMultiline));
- return style;
- }
-
- private void Grid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
- {
- int columnIndex = ((DataGrid)sender).Columns.Count;
- if (columnIndex >= dataContext.Csv.Headers.Count)
- {
- e.Cancel = true;
- return;
- }
-
- DataGridTextColumn column = (DataGridTextColumn)e.Column;
-
- string columnName = dataContext.Csv.Headers[columnIndex];
- column.Header = columnName;
- column.EditingElementStyle = editingStyle;
-
- // Column width
- if (dataContext.Config.Style.Width.ContainsKey(columnName))
- {
- column.Width = dataContext.Config.Style.Width[columnName];
- }
-
- // Conditional formatting
- if (!dataContext.Config.Style.Color.ContainsKey(columnName)) { return; }
- column.CellStyle = new();
-
- foreach (KeyValuePair colorPair in dataContext.Config.Style.Color[columnName])
- {
- DataTrigger trigger = new()
- {
- Binding = new Binding("Column" + columnIndex),
- Value = colorPair.Key
- };
-
- trigger.Setters.Add(
- new Setter()
- {
- Property = BackgroundProperty,
- Value = new BrushConverter().ConvertFromString(colorPair.Value)
- }
- );
- column.CellStyle.Triggers.Add(trigger);
- }
- }
-
- private void Grid_CurrentCellChanged(object sender, System.EventArgs e)
- {
- ((DataGrid)sender).CommitEdit();
- }
-
- private void Grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- var dataGrid = (DataGrid)sender;
- var selected = dataGrid.SelectedItems.Cast().ToList();
-
- dataContext.Status.SelectedItems = selected;
- dataContext.Preview.UpdatePreview();
-
- if (dataContext.Status.ScrollAfterInsert) {
- dataGrid.ScrollIntoView(selected[0]);
- dataContext.Status.ScrollAfterInsert = false;
- }
- }
-}
diff --git a/rowsSharp/Internal/ViewModel/ViewModel.CollectionView.cs b/rowsSharp/Internal/ViewModel/ViewModel.CollectionView.cs
deleted file mode 100644
index 1742737..0000000
--- a/rowsSharp/Internal/ViewModel/ViewModel.CollectionView.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Windows.Data;
-
-namespace rowsSharp.ViewModel;
-internal class RecordsView : INPC
-{
- private ICollectionView collectionView;
- public ICollectionView CollectionView
- {
- get => collectionView;
- set => SetField(ref collectionView, value);
- }
-
- internal RecordsView(ObservableCollection csv)
- {
- collectionView = CollectionViewSource.GetDefaultView(csv);
- }
-}
diff --git a/rowsSharp/Internal/ViewModel/ViewModel.Command.cs b/rowsSharp/Internal/ViewModel/ViewModel.Command.cs
deleted file mode 100644
index 282baa8..0000000
--- a/rowsSharp/Internal/ViewModel/ViewModel.Command.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System.Linq;
-using System.Windows.Controls;
-
-namespace rowsSharp.ViewModel;
-internal class Command
-{
- private DataContext DataContext { get; init; }
-
- internal Command (DataContext dataContext)
- {
- DataContext = dataContext;
- }
-
- public DelegateCommand OutputAlias => new(() => { DataContext.Edit.OutputAliasEditing(); });
-
- public DelegateCommand BeginEdit => new(() => DataContext.Edit.BeginEdit());
- public DelegateCommand EndEdit => new((e) => DataContext.Edit.EndEdit(e));
-
- public DelegateCommand CanInsert => new(
- () => { }, // do nothing
- () => DataContext.Edit.CanInsertTopOrBottom()
- );
- public DelegateCommand InsertTop => new(
- () => DataContext.Edit.Insert(0),
- () => DataContext.Edit.CanInsertTopOrBottom()
- );
- public DelegateCommand InsertAbove => new(
- () => DataContext.Edit.Insert(DataContext.Status.SelectedIndex),
- () => DataContext.Edit.IsAnyRowSelected()
- );
- public DelegateCommand InsertBelow => new(
- () => DataContext.Edit.Insert(DataContext.Status.SelectedIndex + DataContext.Status.SelectedItems.Count),
- () => DataContext.Edit.IsAnyRowSelected()
- );
- public DelegateCommand InsertLast => new(
- () => DataContext.Edit.Insert(-1),
- () => DataContext.Edit.CanInsertTopOrBottom()
- );
- public DelegateCommand Remove => new(
- () => DataContext.Edit.Remove(),
- () => DataContext.Config.CanEdit && DataContext.Status.SelectedIndex != -1
- );
- public DelegateCommand Save => new(
- () => DataContext.Edit.Save(),
- () => DataContext.Config.CanEdit && DataContext.Status.IsDirtyEditor
- );
-
- public DelegateCommand Focus => new(() => DataContext.Filter.FocusFilter());
- public DelegateCommand Filter => new(() => DataContext.Filter.DoFilter());
-
- public DelegateCommand Undo => new(
- () => DataContext.History.Undo(),
- () => DataContext.Config.CanEdit && DataContext.OperationHistory.UndoStack.Any()
- );
- public DelegateCommand Redo => new(
- () => DataContext.History.Redo(),
- () => DataContext.Config.CanEdit && DataContext.OperationHistory.RedoStack.Any()
- );
-
- public DelegateCommand CopyImage => new(
- () => DataContext.Preview.CopyImage(),
- () => DataContext.Status.PreviewBitmap.UriSource is not null
- );
- public DelegateCommand CopyString => new(
- () => DataContext.Preview.CopyString(),
- () => (!string.IsNullOrWhiteSpace(DataContext.Config.CopyRowFormat)) && (DataContext.Status.SelectedIndex != -1)
- );
-}
diff --git a/rowsSharp/Internal/ViewModel/ViewModel.Status.cs b/rowsSharp/Internal/ViewModel/ViewModel.Status.cs
deleted file mode 100644
index 1d24ac2..0000000
--- a/rowsSharp/Internal/ViewModel/ViewModel.Status.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Windows.Media.Imaging;
-using rowsSharp.Model;
-
-namespace rowsSharp.ViewModel;
-
-public class Status : INPC
-{
- public Version Version { get; } = Assembly.GetExecutingAssembly().GetName().Version!;
-
- private bool isDirtyEditor;
- public bool IsDirtyEditor
- {
- get => isDirtyEditor;
- set => SetField(ref isDirtyEditor, value);
- }
-
- private bool isEditing;
- public bool IsEditing
- {
- get => isEditing;
- set => SetField(ref isEditing, value);
- }
-
- private bool isInsertExpanded;
- public bool IsInsertExpanded
- {
- get => isInsertExpanded;
- set => SetField(ref isInsertExpanded, value);
- }
-
- private List selectedItems = new();
- public List SelectedItems
- {
- get => selectedItems;
- set => SetField(ref selectedItems, value);
- }
-
- private int selectedIndex = -1;
- public int SelectedIndex
- {
- get => selectedIndex;
- set => SetField(ref selectedIndex, value);
- }
-
- private bool isFilterFocused;
- public bool IsFilterFocused
- {
- get => isFilterFocused;
- set => SetField(ref isFilterFocused, value);
- }
-
- private string filterText = string.Empty;
- public string FilterText
- {
- get => filterText;
- set => SetField(ref filterText, value);
- }
-
- private BitmapImage previewBitmap = new();
- public BitmapImage PreviewBitmap
- {
- get => previewBitmap;
- set => SetField(ref previewBitmap, value);
- }
-
- internal bool ScrollAfterInsert;
-}
\ No newline at end of file
diff --git a/rowsSharp/Internal/Model/Model.ColumnStyle.cs b/rowsSharp/Model/ColumnStyle.cs
similarity index 87%
rename from rowsSharp/Internal/Model/Model.ColumnStyle.cs
rename to rowsSharp/Model/ColumnStyle.cs
index 6a91bcb..88a0a52 100644
--- a/rowsSharp/Internal/Model/Model.ColumnStyle.cs
+++ b/rowsSharp/Model/ColumnStyle.cs
@@ -2,9 +2,8 @@
namespace rowsSharp.Model;
-public struct ColumnStyle
+public class ColumnStyle
{
- public ColumnStyle() { }
public Dictionary Width { get; init; } = new();
public Dictionary Template { get; init; } = new();
public Dictionary> Alias { get; init; } = new();
diff --git a/rowsSharp/Model/Preferences.cs b/rowsSharp/Model/Preferences.cs
new file mode 100644
index 0000000..ff81d57
--- /dev/null
+++ b/rowsSharp/Model/Preferences.cs
@@ -0,0 +1,47 @@
+using System.IO;
+using System.Windows;
+
+namespace rowsSharp.Model;
+
+public class Preferences : NotifyPropertyChanged
+{
+ public string CsvPath { get; set; } = "";
+ public string CsvName => Path.GetFileName(CsvPath);
+
+ public bool HasHeader { get; init; } = true;
+ public string StylePath { get; set; } = "";
+ public string PreviewPath { get; set; } = "";
+
+ public bool UseInputAlias { get; set; }
+ private bool useOutputAlias;
+ public bool UseOutputAlias
+ {
+ get => useOutputAlias;
+ set => SetField(ref useOutputAlias, value);
+ }
+ public bool UseRegexFilter { get; set; }
+
+ private bool canEdit;
+ public bool CanEdit
+ {
+ get => canEdit;
+ set
+ {
+ SetField(ref canEdit, value);
+ OnPropertyChanged(nameof(IsReadOnly));
+ }
+ }
+ public bool IsReadOnly => !CanEdit;
+ public bool AllowMultiline { get; set; }
+ public bool UseInsertTemplate { get; set; } = true;
+
+ public bool UseAutosave { get; set; } = true;
+ public int AutosavePeriod { get; set; } = 60;
+
+ public bool UseToolTip { get; set; } = true;
+
+ public string ThemePath { get; set; } = "";
+ internal ResourceDictionary? Theme { get; set; }
+
+ public ColumnStyle ColumnStyle { get; set; } = new();
+}
diff --git a/rowsSharp/NLog.config b/rowsSharp/NLog.config
index fa5e65f..68c4b2b 100644
--- a/rowsSharp/NLog.config
+++ b/rowsSharp/NLog.config
@@ -6,11 +6,12 @@
+
-
-
+
+
\ No newline at end of file
diff --git a/rowsSharp/Userdata/CSVData/0.png b/rowsSharp/Userdata/CSVData/0.png
deleted file mode 100644
index cb3157c..0000000
Binary files a/rowsSharp/Userdata/CSVData/0.png and /dev/null differ
diff --git a/rowsSharp/Userdata/CSVData/100.png b/rowsSharp/Userdata/CSVData/100.png
deleted file mode 100644
index 80a1ae0..0000000
Binary files a/rowsSharp/Userdata/CSVData/100.png and /dev/null differ
diff --git a/rowsSharp/Userdata/CSVData/300.png b/rowsSharp/Userdata/CSVData/300.png
deleted file mode 100644
index f9fb1aa..0000000
Binary files a/rowsSharp/Userdata/CSVData/300.png and /dev/null differ
diff --git a/rowsSharp/Userdata/CSVData/500.png b/rowsSharp/Userdata/CSVData/500.png
deleted file mode 100644
index 47b79eb..0000000
Binary files a/rowsSharp/Userdata/CSVData/500.png and /dev/null differ
diff --git a/rowsSharp/Userdata/CSVData/A1.png b/rowsSharp/Userdata/CSVData/A1.png
new file mode 100644
index 0000000..8c6bdd6
Binary files /dev/null and b/rowsSharp/Userdata/CSVData/A1.png differ
diff --git a/rowsSharp/Userdata/CSVData/A2.png b/rowsSharp/Userdata/CSVData/A2.png
new file mode 100644
index 0000000..90680a5
Binary files /dev/null and b/rowsSharp/Userdata/CSVData/A2.png differ
diff --git a/rowsSharp/Userdata/CSVData/A3.png b/rowsSharp/Userdata/CSVData/A3.png
new file mode 100644
index 0000000..80a5971
Binary files /dev/null and b/rowsSharp/Userdata/CSVData/A3.png differ
diff --git a/rowsSharp/Userdata/CSVData/data.csv b/rowsSharp/Userdata/CSVData/data.csv
index 6462fa9..c63bf99 100644
--- a/rowsSharp/Userdata/CSVData/data.csv
+++ b/rowsSharp/Userdata/CSVData/data.csv
@@ -1,21 +1,21 @@
-"Train","Start","End","Top Speed","JNR","JR Central","JR West","JR Kyushu","JR East","JR Hokkaido"
-"0","1964","2008","220","1","1","1","0","0","0"
-"100","1985","2012","220","1","1","1","0","0","0"
-"300","1992","2012","270","0","1","1","0","0","0"
-"500","1997","","300","0","0","1","0","0","0"
-"700","1999","","300","0","1","1","0","0","0"
-"N700","2007","","300","0","1","1","1","0","0"
-"N700A","2012","","300","0","1","1","0","0","0"
-"N700S","2020","","365","0","1","1","1","0","0"
-"800","2004","","260","0","0","0","1","0","0"
-"200","1982","2013","275","1","0","0","0","1","0"
-"400","1992","2010","240","0","0","0","0","1","0"
-"E1","1994","2012","240","0","0","0","0","1","0"
-"E2","1997","","275","0","0","0","0","1","0"
-"E3","1997","","275","0","0","0","0","1","0"
-"E4","1997","","240","0","0","0","0","1","0"
-"E5","2011","","320","0","0","0","0","1","0"
-"E6","2013","","360","0","0","0","0","1","0"
-"H5","2016","","320","0","0","0","0","0","1"
-"E7","2014","","275","0","0","0","0","1","0"
-"W7","2014","","275","0","0","1","0","0","0"
\ No newline at end of file
+A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P
+A1,B1,C1,D1,E1,F1,G1,H1,I1,J1,K1,L1,M1,N1,O1
+A2,B2,C2,.,.,.,G2,H2,I2,.,.,.,M2,N2,O2
+A3,B3,.,.,.,.,.,H3,.,.,.,.,.,N3,O3
+A4,.,.,.,.,.,.,.,.,.,.,.,.,.,O4
+A5,.,.,.,.,.,.,.,.,.,.,.,.,.,O5
+A6,.,.,.,.,.,.,.,.,.,.,.,.,.,O6
+A7,B7,.,.,.,.,.,.,.,.,.,.,.,N7,O7
+A8,B8,C8,.,.,.,.,.,.,.,.,.,M8,N8,O8
+A9,B9,C9,D9,.,.,.,.,.,.,.,L9,M9,N9,O9
+A10,B10,C10,D10,E10,.,.,.,.,.,K10,L10,M10,N10,O10
+A11,B11,C11,D11,E11,F11,.,.,.,J11,K11,L11,M11,N11,O11
+A12,B12,C12,D12,E12,F12,G12,.,I12,J12,K12,L12,M12,N12,O12
+A13,B13,C13,D13,E13,F13,G13,H13,I13,J13,K13,L13,M13,N13,O13
+A14,B14,C14,D14,E14,F14,G14,H14,I14,J14,K14,L14,M14,N14,O14
+A15,B15,C15,D15,E15,F15,G15,H15,I15,J15,K15,L15,M15,N15,O15
+A16,B16,C16,D16,E16,F16,G16,H16,I16,J16,K16,L16,M16,N16,O16
+A17,B17,C17,D17,E17,F17,G17,H17,I17,J17,K17,L17,M17,N17,O17
+A18,B18,C18,D18,E18,F18,G18,H18,I18,J18,K18,L18,M18,N18,O18
+A19,B19,C19,D19,E19,F19,G19,H19,I19,J19,K19,L19,M19,N19,O19
+A20,B20,C20,D20,E20,F20,G20,H20,I20,J20,K20,L20,M20,N20,O20
diff --git a/rowsSharp/Userdata/Configurations/Configuration.json b/rowsSharp/Userdata/Configurations/Configuration.json
index 67e2bbc..39c9098 100644
--- a/rowsSharp/Userdata/Configurations/Configuration.json
+++ b/rowsSharp/Userdata/Configurations/Configuration.json
@@ -3,8 +3,8 @@
"HasHeader": true,
"StylePath": "$baseDir/Configurations/Style.json",
- "PreviewPath": "$baseDir/CSVData/.png",
- "CopyRowFormat": ": km/h",
+ "ThemePath": "$baseDir/Configurations/Themes/Light.xaml",
+ "PreviewPath": "$baseDir/CSVData/.png",
"UseInputAlias": true,
"UseOutputAlias": false,
@@ -12,10 +12,8 @@
"UseToolTip": true,
"CanEdit": true,
- "AllowMultiline": true,
- "InsertSelectedCount": true,
- "InsertCount": 10,
+ "AllowMultiline": false,
"UseInsertTemplate": true,
-
- "ThemePath": "$baseDir/Configurations/Themes/Light.xaml"
+ "UseAutosave": false,
+ "AutosavePeriod": 60
}
\ No newline at end of file
diff --git a/rowsSharp/Userdata/Configurations/Style.json b/rowsSharp/Userdata/Configurations/Style.json
index 93eeb4e..e23a92d 100644
--- a/rowsSharp/Userdata/Configurations/Style.json
+++ b/rowsSharp/Userdata/Configurations/Style.json
@@ -1,66 +1,95 @@
{
"Width": {
- "Train": 196,
- "Start": 96,
- "End": 96,
- "Top Speed": 196
+ "A": 34,
+ "B": 34,
+ "C": 34,
+ "D": 34,
+ "E": 34,
+ "F": 34,
+ "G": 34,
+ "H": 34,
+ "I": 34,
+ "J": 34,
+ "K": 34,
+ "L": 34,
+ "M": 34,
+ "N": 34,
+ "O": 34,
+ "P": 340
},
"Template": {
- "Train": "",
- "Start": "",
- "End": "",
- "Top Speed": "",
- "JNR": "<#>",
- "JR Central": ""
+ "P": " <#> "
},
"Alias": {
- "JNR": {
- "0": "No",
- "1": "Yes"
- },
+ "B": { ".": "♡" },
+ "C": { ".": "♡" },
+ "D": { ".": "♡" },
+ "E": { ".": "♡" },
+ "F": { ".": "♡" },
+ "G": { ".": "♡" },
+ "H": { ".": "♡" },
+ "I": { ".": "♡" },
+ "J": { ".": "♡" },
+ "K": { ".": "♡" },
+ "L": { ".": "♡" },
+ "M": { ".": "♡" },
+ "N": { ".": "♡" }
+ },
- "JR Central": {
- "0": "No",
- "1": "Yes"
+ "Color": {
+ "B": {
+ ".": "#80ff0000",
+ "♡": "#80ff0000"
},
-
- "JR West": {
- "0": "No",
- "1": "Yes"
+ "C": {
+ ".": "#80ff3700",
+ "♡": "#80ff3700"
},
-
- "JR Kyushu": {
- "0": "No",
- "1": "Yes"
+ "D": {
+ ".": "#80ff5200",
+ "♡": "#80ff5200"
},
-
- "JR Hokkaido": {
- "0": "No",
- "1": "Yes"
- }
- },
-
- "Color": {
- "JNR": {
- "1": "#80B7E1CD",
- "Yes": "#80B7E1CD"
+ "E": {
+ ".": "#80ff6700",
+ "♡": "#80ff6700"
},
-
- "JR Central": {
- "1": "#80E1B7CD",
- "Yes": "#80E1B7CD"
+ "F": {
+ ".": "#80ff7b00",
+ "♡": "#80ff7b00"
},
-
- "JR West": {
- "1": "#80CDB7E1",
- "Yes": "#80CDB7E1"
+ "G": {
+ ".": "#80ff8d00",
+ "♡": "#80ff8d00"
},
-
- "JR Kyushu": {
- "1": "#80B7E1CD",
- "Yes": "#80B7E1CD"
+ "H": {
+ ".": "#80ff9f00",
+ "♡": "#80ff9f00"
+ },
+ "I": {
+ ".": "#80ffb000",
+ "♡": "#80ffb000"
+ },
+ "J": {
+ ".": "#80ffc000",
+ "♡": "#80ffc000"
+ },
+ "K": {
+ ".": "#80ffd000",
+ "♡": "#80ffd000"
+ },
+ "L": {
+ ".": "#80ffe000",
+ "♡": "#80ffe000"
+ },
+ "M": {
+ ".": "#80fff000",
+ "♡": "#80fff000"
+ },
+ "N": {
+ ".": "#80ffff00",
+ "♡": "#80ffff00"
}
}
}
\ No newline at end of file
diff --git a/rowsSharp/Userdata/Configurations/Themes/AMOLED.xaml b/rowsSharp/Userdata/Configurations/Themes/AMOLED.xaml
index 02fe3a7..1e2c57e 100644
--- a/rowsSharp/Userdata/Configurations/Themes/AMOLED.xaml
+++ b/rowsSharp/Userdata/Configurations/Themes/AMOLED.xaml
@@ -6,6 +6,9 @@
+
+
+
@@ -14,15 +17,16 @@
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/rowsSharp/Userdata/Configurations/Themes/Bleach.xaml b/rowsSharp/Userdata/Configurations/Themes/Bleach.xaml
index 773b7f0..1d22b86 100644
--- a/rowsSharp/Userdata/Configurations/Themes/Bleach.xaml
+++ b/rowsSharp/Userdata/Configurations/Themes/Bleach.xaml
@@ -4,7 +4,7 @@
512
512
- 1
+ 3
32
*
Comics Sans MS,Impact
diff --git a/rowsSharp/Userdata/Configurations/Themes/Dark.xaml b/rowsSharp/Userdata/Configurations/Themes/Dark.xaml
index 404d239..2bbc66b 100644
--- a/rowsSharp/Userdata/Configurations/Themes/Dark.xaml
+++ b/rowsSharp/Userdata/Configurations/Themes/Dark.xaml
@@ -1,47 +1,44 @@
-
-
-
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
diff --git a/rowsSharp/Userdata/Configurations/Themes/Light.xaml b/rowsSharp/Userdata/Configurations/Themes/Light.xaml
index 4890b1a..a1d14a0 100644
--- a/rowsSharp/Userdata/Configurations/Themes/Light.xaml
+++ b/rowsSharp/Userdata/Configurations/Themes/Light.xaml
@@ -1,60 +1,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/Column.cs b/rowsSharp/View/Column.cs
new file mode 100644
index 0000000..06d85a3
--- /dev/null
+++ b/rowsSharp/View/Column.cs
@@ -0,0 +1,97 @@
+using Microsoft.Xaml.Behaviors;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Windows.Controls;
+using System.Windows;
+
+namespace rowsSharp.View;
+
+/* Code: [ColumnsBindingBehavior](https://stackoverflow.com/a/40935553)
+ * Creator: [Paul Gibson](https://stackoverflow.com/users/4024800/paul-gibson)
+ * License: [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)
+ * Modifications:
+ * - Code formatting
+ * - Removing unnecessary bits
+ */
+
+public class ColumnsBindingBehaviour : Behavior
+{
+ public ObservableCollection Columns
+ {
+ get => (ObservableCollection)GetValue(ColumnsProperty);
+ set => SetValue(ColumnsProperty, value);
+ }
+
+ public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns",
+ typeof(ObservableCollection), typeof(ColumnsBindingBehaviour),
+ new(OnDataGridColumnsPropertyChanged));
+
+ private static void OnDataGridColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
+ {
+ var context = (ColumnsBindingBehaviour)source;
+
+ if (e.OldValue is ObservableCollection oldItems)
+ {
+ RemoveColumns(context, oldItems);
+ }
+
+ if (e.NewValue is ObservableCollection newItems)
+ {
+ AddColumns(context, newItems);
+ }
+ }
+
+ private static void RemoveColumns(ColumnsBindingBehaviour context, ObservableCollection columns)
+ {
+ foreach (var column in columns)
+ {
+ context._datagridColumns.Remove(column);
+ }
+ columns.CollectionChanged -= context.CollectionChanged;
+ }
+
+ private static void AddColumns(ColumnsBindingBehaviour context, ObservableCollection columns)
+ {
+ foreach (var column in columns)
+ {
+ context._datagridColumns.Add(column);
+ }
+ columns.CollectionChanged += context.CollectionChanged;
+ }
+
+ private ObservableCollection _datagridColumns = new();
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+ _datagridColumns = AssociatedObject.Columns;
+ }
+
+ private void CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ if (e.NewItems != null)
+ foreach (DataGridColumn one in e.NewItems)
+ _datagridColumns.Insert(e.NewStartingIndex, one);
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ if (e.OldItems != null)
+ _datagridColumns.RemoveAt(e.OldStartingIndex);
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ _datagridColumns.Move(e.OldStartingIndex, e.NewStartingIndex);
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ _datagridColumns.Clear();
+ if (e.NewItems != null)
+ foreach (DataGridColumn one in e.NewItems)
+ _datagridColumns.Add(one);
+ break;
+ }
+ }
+}
diff --git a/rowsSharp/View/Editor.xaml b/rowsSharp/View/Editor.xaml
new file mode 100644
index 0000000..9f9fdf3
--- /dev/null
+++ b/rowsSharp/View/Editor.xaml
@@ -0,0 +1,423 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rowsSharp/View/Editor.xaml.cs b/rowsSharp/View/Editor.xaml.cs
new file mode 100644
index 0000000..fc5f62d
--- /dev/null
+++ b/rowsSharp/View/Editor.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace rowsSharp.View;
+
+///
+/// Interaction logic for Home.xaml
+///
+public partial class Editor : UserControl
+{
+ public Editor()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/rowsSharp/View/Font/LICENSE b/rowsSharp/View/Font/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/rowsSharp/View/Font/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/rowsSharp/View/Font/material-symbols-outlined.otf b/rowsSharp/View/Font/material-symbols-outlined.otf
new file mode 100644
index 0000000..acb29e7
Binary files /dev/null and b/rowsSharp/View/Font/material-symbols-outlined.otf differ
diff --git a/rowsSharp/Internal/View/Light.xaml b/rowsSharp/View/Light.xaml
similarity index 70%
rename from rowsSharp/Internal/View/Light.xaml
rename to rowsSharp/View/Light.xaml
index 7773023..f245267 100644
--- a/rowsSharp/Internal/View/Light.xaml
+++ b/rowsSharp/View/Light.xaml
@@ -1,48 +1,47 @@
-
+
1400
600
- 1
- 25
- *
+ 0
+ 33
+ 2*
+ *
Courier New,Roboto Mono
16
- 14
-
-
+ 22
+ 12
+
+
-
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/rowsSharp/View/MainWindow.xaml b/rowsSharp/View/MainWindow.xaml
new file mode 100644
index 0000000..2d471c4
--- /dev/null
+++ b/rowsSharp/View/MainWindow.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/rowsSharp/View/MainWindow.xaml.cs b/rowsSharp/View/MainWindow.xaml.cs
new file mode 100644
index 0000000..6edfe97
--- /dev/null
+++ b/rowsSharp/View/MainWindow.xaml.cs
@@ -0,0 +1,17 @@
+using rowsSharp.ViewModel;
+using System.Windows;
+
+namespace rowsSharp.View;
+
+///
+/// Interaction logic for MainWindow.xaml
+///
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ DataContext = new RootVM();
+ InitializeComponent();
+ App.Logger.Info("Okay, it's happening! Everybody stay calm!");
+ }
+}
diff --git a/rowsSharp/View/RenameColumn.xaml b/rowsSharp/View/RenameColumn.xaml
new file mode 100644
index 0000000..94ea48a
--- /dev/null
+++ b/rowsSharp/View/RenameColumn.xaml
@@ -0,0 +1,21 @@
+
+
+
+ Enter new column name
+
+
+
+
+
+
+
+
diff --git a/rowsSharp/View/RenameColumn.xaml.cs b/rowsSharp/View/RenameColumn.xaml.cs
new file mode 100644
index 0000000..cdfc042
--- /dev/null
+++ b/rowsSharp/View/RenameColumn.xaml.cs
@@ -0,0 +1,27 @@
+using System.Windows;
+
+namespace rowsSharp.View
+{
+ // MVVM is an overkill here, hence not implemented.
+
+ public partial class RenameColumn : Window
+ {
+ public RenameColumn()
+ {
+ InitializeComponent();
+ }
+
+ public string? NewName { get; set; }
+
+ private void Rename_Click(object sender, RoutedEventArgs e)
+ {
+ NewName = ColumnName.Text;
+ DialogResult = true;
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+ }
+}
diff --git a/rowsSharp/View/ResourceDictionary/Button.xaml b/rowsSharp/View/ResourceDictionary/Button.xaml
new file mode 100644
index 0000000..e1bad4d
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/Button.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/DataGrid.xaml b/rowsSharp/View/ResourceDictionary/DataGrid.xaml
new file mode 100644
index 0000000..6bd2bf9
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/DataGrid.xaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/OptionMark.xaml b/rowsSharp/View/ResourceDictionary/OptionMark.xaml
new file mode 100644
index 0000000..dbd0806
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/OptionMark.xaml
@@ -0,0 +1,72 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/Scrollbar.xaml b/rowsSharp/View/ResourceDictionary/Scrollbar.xaml
new file mode 100644
index 0000000..adf5a60
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/Scrollbar.xaml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/TextBlock.xaml b/rowsSharp/View/ResourceDictionary/TextBlock.xaml
new file mode 100644
index 0000000..073be25
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/TextBlock.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/TextBox.xaml b/rowsSharp/View/ResourceDictionary/TextBox.xaml
new file mode 100644
index 0000000..00dc979
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/TextBox.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/ResourceDictionary/ToggleButton.xaml b/rowsSharp/View/ResourceDictionary/ToggleButton.xaml
new file mode 100644
index 0000000..28fe5ac
--- /dev/null
+++ b/rowsSharp/View/ResourceDictionary/ToggleButton.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/Settings.xaml b/rowsSharp/View/Settings.xaml
new file mode 100644
index 0000000..43f6ec9
--- /dev/null
+++ b/rowsSharp/View/Settings.xaml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Input Alias
+ Convert aliased filters to match raw CSV data
+
+
+
+
+
+
+
+
+
+
+
+
+ Output Alias
+ Alias raw CSV data when displaying
+
+
+
+
+
+
+
+
+ Regular Expression
+ Parse regular expressions in filters
+
+
+
+
+
+
+
+
+
+
+ Editing
+ Enabling editing will disable Output Alias
+
+
+
+
+
+
+
+
+ Insertion Template
+ Template rows when inserting
+
+
+
+
+
+
+
+
+
+
+
+
+ Autosave
+ Automatically save changes periodically
+
+
+
+
+
+
+
+
+
+
+ Show Tooltips
+ Show helpful text on obsecure gotcha's
+
+
+
+
+
+
diff --git a/rowsSharp/View/Settings.xaml.cs b/rowsSharp/View/Settings.xaml.cs
new file mode 100644
index 0000000..a7d1d0d
--- /dev/null
+++ b/rowsSharp/View/Settings.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace rowsSharp.View;
+
+///
+/// Interaction logic for Settings.xaml
+///
+public partial class Settings : UserControl
+{
+ public Settings()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/rowsSharp/View/Splash.xaml b/rowsSharp/View/Splash.xaml
new file mode 100644
index 0000000..8f47371
--- /dev/null
+++ b/rowsSharp/View/Splash.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowsSharp/View/Splash.xaml.cs b/rowsSharp/View/Splash.xaml.cs
new file mode 100644
index 0000000..44e4520
--- /dev/null
+++ b/rowsSharp/View/Splash.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace rowsSharp.View;
+
+///
+/// Interaction logic for Splash.xaml
+///
+public partial class Splash : UserControl
+{
+ public Splash()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/rowsSharp/View/Welcome.xaml b/rowsSharp/View/Welcome.xaml
new file mode 100644
index 0000000..1661251
--- /dev/null
+++ b/rowsSharp/View/Welcome.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rowsSharp/View/Welcome.xaml.cs b/rowsSharp/View/Welcome.xaml.cs
new file mode 100644
index 0000000..cc72db8
--- /dev/null
+++ b/rowsSharp/View/Welcome.xaml.cs
@@ -0,0 +1,13 @@
+using System.Windows.Controls;
+
+namespace rowsSharp.View;
+///
+/// Interaction logic for Welcome.xaml
+///
+public partial class Welcome : UserControl
+{
+ public Welcome()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/rowsSharp/ViewModel/Editor/Edit.cs b/rowsSharp/ViewModel/Editor/Edit.cs
new file mode 100644
index 0000000..3e2ec8d
--- /dev/null
+++ b/rowsSharp/ViewModel/Editor/Edit.cs
@@ -0,0 +1,318 @@
+using ObservableTable.Core;
+using rowsSharp.Domain;
+using rowsSharp.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Timers;
+using rowsSharp.View;
+using System.Windows.Controls;
+using System.Windows;
+
+namespace rowsSharp.ViewModel.Editor;
+
+public class Edit : NotifyPropertyChanged, IDisposable
+{
+ private readonly RootVM rootVM;
+ private Preferences Preferences => rootVM.Preferences;
+ private ObservableTable Table => rootVM.Table;
+
+ private readonly UniqueColumn column = new();
+ private readonly Timer timer;
+
+ public Edit(RootVM rootViewModel)
+ {
+ rootVM = rootViewModel;
+
+ Table.TableModified += Table_TableModified;
+
+ timer = new(Preferences.AutosavePeriod * 1000);
+ timer.Enabled = true;
+ timer.AutoReset = true;
+ timer.Elapsed += Timer_Elapsed;
+ }
+
+ public void Dispose()
+ {
+ timer.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
+ {
+ if (!Preferences.UseAutosave || !Preferences.CanEdit || !isEditorDirty) { return; }
+
+ CsvFile.Export(Preferences.CsvPath, Table, Preferences.HasHeader);
+ IsEditorDirty = false;
+ }
+
+ private void Table_TableModified(object? sender, EventArgs e)
+ {
+ IsEditorDirty = true;
+ }
+
+ private bool isEditorDirty;
+ public bool IsEditorDirty
+ {
+ get => isEditorDirty;
+ private set => SetField(ref isEditorDirty, value);
+ }
+
+
+ private bool DataGridEditableAndSelected()
+ {
+ return Preferences.CanEdit && selectedCells.Count > 0;
+ }
+
+ public DelegateCommand Undo => new(
+ () => Table.Undo(),
+ () => Preferences.CanEdit && Table.UndoCount > 0
+ );
+
+ public DelegateCommand Redo => new(
+ () => Table.Redo(),
+ () => Preferences.CanEdit && Table.RedoCount > 0
+ );
+
+ private enum InsertionMode
+ {
+ Prepend,
+ Before,
+ After,
+ Append
+ }
+
+ private void InsertRow(InsertionMode insertionMode)
+ {
+ int count = selectedCells.Count > 0
+ ? selectedCells.Rows().Count()
+ : 1;
+
+ int index = insertionMode switch
+ {
+ InsertionMode.Prepend => 0,
+ InsertionMode.Before => selectedCells.RowIndices(Table.Records).Min(),
+ InsertionMode.After => selectedCells.RowIndices(Table.Records).Max() + 1,
+ _ => Table.Records.Count
+ };
+
+ var template = Preferences.UseInsertTemplate ? Preferences.ColumnStyle?.Template : null;
+
+ var toAdd = RowTemplate.Generate(count, Table.Headers, template);
+ Table.InsertRow(index, toAdd);
+ }
+
+ public DelegateCommand InsertRowAbove => new(
+ () => InsertRow(InsertionMode.Before),
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand InsertRowBelow => new(
+ () => InsertRow(InsertionMode.After),
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand InsertRowFirst => new(
+ () => InsertRow(InsertionMode.Prepend),
+ () => Preferences.CanEdit
+ );
+
+ public DelegateCommand InsertRowLast => new(
+ () => InsertRow(InsertionMode.Append),
+ () => Preferences.CanEdit
+ );
+
+ public DelegateCommand NewTable => new(
+ () =>
+ {
+ InsertColumn(InsertionMode.Append);
+ InsertRow(InsertionMode.Append);
+ },
+ () => Preferences.CanEdit
+ );
+
+ private void InsertColumn(InsertionMode insertionMode)
+ {
+ int count = selectedCells.Count > 0
+ ? selectedCells.Columns().Count()
+ : 1;
+
+ int index = insertionMode switch
+ {
+ InsertionMode.Prepend => 0,
+ InsertionMode.Before => selectedCells.ColumnIndices(Table.Headers).Min(),
+ InsertionMode.After => selectedCells.ColumnIndices(Table.Headers).Max() + 1,
+ _ => Table.Headers.Count
+ };
+
+ Table.InsertColumn(index, column.Next(count));
+ }
+
+ public DelegateCommand InsertColumnLeft => new(
+ () => InsertColumn(InsertionMode.Before),
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand InsertColumnRight => new(
+ () => InsertColumn(InsertionMode.After),
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand InsertColumnFirst => new(
+ () => InsertColumn(InsertionMode.Prepend),
+ () => Preferences.CanEdit
+ );
+
+ public DelegateCommand InsertColumnLast => new(
+ () => InsertColumn(InsertionMode.Append),
+ () => Preferences.CanEdit
+ );
+
+ public DelegateCommand RemoveRows => new(
+ () => Table.RemoveRow(selectedCells.Rows()),
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand RemoveColumns => new(
+ () => Table.RemoveColumn(selectedCells.Columns()),
+ () => DataGridEditableAndSelected()
+ );
+
+ private IList selectedCells = new List();
+ public IList SelectedCells
+ {
+ get => selectedCells;
+ set
+ {
+ SetField(ref selectedCells, value);
+ OnPropertyChanged(nameof(SelectedRows));
+ OnPropertyChanged(nameof(SelectedColumns));
+ }
+ }
+
+ public DelegateCommand> ChangeSelectedCells => new(
+ (cells) => { SelectedCells = cells; }
+ );
+
+ public int SelectedRows => selectedCells.Rows().Count();
+ public int SelectedColumns => selectedCells.Columns().Count();
+
+ public DelegateCommand Save => new(
+ () =>
+ {
+ CsvFile.Export(Preferences.CsvPath, Table, Preferences.HasHeader);
+ IsEditorDirty = false;
+ },
+ () => Preferences.CanEdit && isEditorDirty
+ );
+
+ public DelegateCommand RenameColumn => new(
+ () =>
+ {
+ RenameColumn dialog = new();
+ dialog.ShowDialog();
+ string? newHeader = dialog.NewName;
+
+ if (newHeader is null) { return; }
+
+ int index = Table.Headers.IndexOf((string)selectedCells[0].Column.Header);
+ Table.RenameColumn(index, newHeader);
+ },
+ () => DataGridEditableAndSelected() && Preferences.HasHeader
+ );
+
+ private IEnumerable> EmptiedSelectedCells()
+ {
+ foreach (var cell in selectedCells)
+ {
+ int row = cell.RowIndex(Table.Records);
+ int column = cell.ColumnIndex(Table.Headers);
+
+ yield return new(row, column, null);
+ }
+ }
+
+ public DelegateCommand ClearCells => new(
+ () => Table.SetCell(EmptiedSelectedCells()),
+ () => DataGridEditableAndSelected()
+ );
+
+ private IEnumerable> EmptiedSelectedRows()
+ {
+ foreach (int row in selectedCells.RowIndices(Table.Records))
+ {
+ for (int i = 0; i < Table.Headers.Count; i++)
+ {
+ yield return new(row, i, null);
+ }
+ }
+ }
+
+ public DelegateCommand ClearRows => new(
+ () => Table.SetCell(EmptiedSelectedRows()),
+ () => DataGridEditableAndSelected()
+ );
+
+ private IEnumerable> EmptiedSelectedColumns()
+ {
+ foreach (int column in selectedCells.ColumnIndices(Table.Headers))
+ {
+ for (int i = 0; i < Table.Records.Count; i++)
+ {
+ yield return new(i, column, null);
+ }
+ }
+ }
+
+ public DelegateCommand ClearColumns => new(
+ () => Table.SetCell(EmptiedSelectedColumns()),
+ () => DataGridEditableAndSelected()
+ );
+
+ private static IEnumerable ParseClipboard()
+ {
+ string clipboard = Clipboard.GetText().ReplaceLineEndings().Trim();
+ string[] lines = clipboard.Split(Environment.NewLine);
+
+ foreach (var line in lines)
+ {
+ yield return line.Split('\t');
+ }
+ }
+
+ private IEnumerable> YieldPasteCells(IList toPaste, int rowOffset, int columnOffset)
+ {
+ for (int row = 0; row < toPaste.Count; row++)
+ {
+ if (rowOffset + row > Table.Records.Count - 1) { continue; }
+ for (int column = 0; column < toPaste[row].Length; column++)
+ {
+ if (columnOffset + column > Table.Headers.Count - 1) { continue; }
+ yield return new(rowOffset + row, columnOffset + column, toPaste[row][column]);
+ }
+ }
+ }
+
+ public DelegateCommand Paste => new(
+ () =>
+ {
+ int rowOffset = selectedCells.RowIndices(Table.Records).Min();
+ int columnOffset = selectedCells.ColumnIndices(Table.Headers).Min();
+
+ var toPaste = ParseClipboard().ToList();
+ Table.SetCell(YieldPasteCells(toPaste, rowOffset, columnOffset));
+ },
+ () => DataGridEditableAndSelected()
+ );
+
+ public DelegateCommand ReorderColumn => new(
+ (e) =>
+ {
+ int oldIndex = Table.Headers.IndexOf((string)e.Column.Header);
+ int newIndex = e.Column.DisplayIndex;
+
+ Table.ReorderColumn(oldIndex, newIndex);
+ },
+ (e) => Preferences.CanEdit
+ );
+}
diff --git a/rowsSharp/ViewModel/Editor/Editor.cs b/rowsSharp/ViewModel/Editor/Editor.cs
new file mode 100644
index 0000000..af7dbcd
--- /dev/null
+++ b/rowsSharp/ViewModel/Editor/Editor.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Data;
+using ObservableTable.Core;
+using rowsSharp.Domain;
+using rowsSharp.Model;
+using rowsSharp.ViewModel.Editor;
+
+namespace rowsSharp.ViewModel;
+
+public class EditorVM : NotifyPropertyChanged
+{
+ private readonly RootVM rootVM;
+ private Preferences Preferences => rootVM.Preferences;
+ private ObservableTable Table => rootVM.Table;
+
+ public Edit Edit { get; set; }
+ public Editor.Filter Filter { get; set; }
+ public Editor.Preview Preview { get; set; }
+
+ public EditorVM(RootVM rootViewModel)
+ {
+ rootVM = rootViewModel;
+ collectionView = CollectionViewSource.GetDefaultView(Table.Records);
+
+ Edit = new(rootVM);
+ Filter = new(rootVM, CollectionView);
+ Preview = new(rootVM);
+
+ ((INotifyCollectionChanged)Table.Headers).CollectionChanged += Headers_CollectionChanged;
+ }
+
+ private void Headers_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnPropertyChanged(nameof(DataGridColumns));
+ }
+
+ private ICollectionView collectionView;
+ public ICollectionView CollectionView
+ {
+ get => collectionView;
+ set => SetField(ref collectionView, value);
+ }
+
+ private const int DefaultColumnWidth = 128;
+
+ public ObservableCollection DataGridColumns =>
+ new(
+ Table.Headers.Select((header, index) => new DataGridTextColumn()
+ {
+ Header = header,
+ Binding = new Binding("[" + index + "]"),
+ Width = Preferences.ColumnStyle.Width.GetValueOrDefault(header, DefaultColumnWidth),
+
+ CellStyle = Preferences.ColumnStyle.Color.GetConditionalFormatting(header, index),
+ EditingElementStyle = ColumnStyleHelper.GetEditingElementStyle(Preferences.AllowMultiline)
+ })
+ );
+}
diff --git a/rowsSharp/ViewModel/Editor/Filter.cs b/rowsSharp/ViewModel/Editor/Filter.cs
new file mode 100644
index 0000000..3aeb44c
--- /dev/null
+++ b/rowsSharp/ViewModel/Editor/Filter.cs
@@ -0,0 +1,72 @@
+using ObservableTable.Core;
+using rowsSharp.Model;
+using rowsSharp.View;
+using System.ComponentModel;
+using System.Windows.Controls;
+
+namespace rowsSharp.ViewModel.Editor;
+
+public class Filter : NotifyPropertyChanged
+{
+ private readonly RootVM rootVM;
+ private Preferences Preferences => rootVM.Preferences;
+ private ObservableTable Table => rootVM.Table;
+
+ private readonly Domain.Filter filter;
+
+ public Filter(RootVM rootViewModel, ICollectionView collectionView)
+ {
+ rootVM = rootViewModel;
+ filter = new(collectionView);
+ filter.Alias = Preferences.ColumnStyle?.Alias ?? new();
+ }
+
+ private string filterText = "";
+ public string FilterText
+ {
+ get => filterText;
+ set => SetField(ref filterText, value);
+ }
+
+ public DelegateCommand InvokeFilter => new(() =>
+ {
+ filter.UseRegex = Preferences.UseRegexFilter;
+ filter.UseInputAlias = Preferences.UseInputAlias;
+ filter.UseOutputAlias = Preferences.IsReadOnly && Preferences.UseOutputAlias;
+ filter.Headers = Table.Headers;
+ filter.FilterText = FilterText;
+ rootVM.EditorVM!.CollectionView = filter.Invoke();
+ });
+
+ private bool? isFilterFocused;
+ public bool? IsFilterFocused
+ {
+ get => isFilterFocused;
+ set => SetField(ref isFilterFocused, value);
+ }
+
+ public DelegateCommand FocusFilter => new(
+ () =>
+ {
+ IsFilterFocused = false;
+ IsFilterFocused = true;
+ }
+ );
+
+ public DelegateCommand OpenPreferences => new(() =>
+ {
+ rootVM.SettingsVM = new(rootVM, rootVM.CurrentUserControl);
+ rootVM.CurrentUserControl = new Settings();
+ });
+
+ public DelegateCommand ResetSorting => new(
+ (datagrid) =>
+ {
+ rootVM.EditorVM?.CollectionView.SortDescriptions.Clear();
+ foreach (var column in datagrid.Columns)
+ {
+ column.SortDirection = null;
+ }
+ }
+ );
+}
diff --git a/rowsSharp/ViewModel/Editor/Preview.cs b/rowsSharp/ViewModel/Editor/Preview.cs
new file mode 100644
index 0000000..90421d1
--- /dev/null
+++ b/rowsSharp/ViewModel/Editor/Preview.cs
@@ -0,0 +1,42 @@
+using rowsSharp.Domain;
+using rowsSharp.Model;
+using System.Collections.Generic;
+using System.Windows.Media.Imaging;
+using System.Windows;
+using ObservableTable.Core;
+
+namespace rowsSharp.ViewModel.Editor;
+
+public class Preview : NotifyPropertyChanged
+{
+ private readonly RootVM rootVM;
+ private Preferences Preferences => rootVM.Preferences;
+ private ObservableTable Table => rootVM.Table;
+
+ public Preview(RootVM rootViewModel)
+ {
+ rootVM = rootViewModel;
+ }
+
+ public DelegateCommand> ChangePreview => new(
+ (item) =>
+ {
+ string path = Preferences.PreviewPath;
+ path = ColumnNotation.Expand(path, Table.Headers, item);
+
+ Bitmap = Domain.Preview.FromPath(path);
+ }
+ );
+
+ public DelegateCommand CopyPreview => new(
+ () => Clipboard.SetImage(bitmap),
+ () => Bitmap is not null
+ );
+
+ private BitmapImage? bitmap;
+ public BitmapImage? Bitmap
+ {
+ get => bitmap;
+ set => SetField(ref bitmap, value);
+ }
+}
diff --git a/rowsSharp/ViewModel/Root.cs b/rowsSharp/ViewModel/Root.cs
new file mode 100644
index 0000000..9a04a4a
--- /dev/null
+++ b/rowsSharp/ViewModel/Root.cs
@@ -0,0 +1,105 @@
+using rowsSharp.Domain;
+using rowsSharp.Model;
+using System.Reflection;
+using System;
+using System.Windows.Controls;
+using rowsSharp.View;
+using System.Threading;
+using System.Windows;
+using System.Threading.Tasks;
+using ObservableTable.Core;
+using System.ComponentModel;
+
+namespace rowsSharp.ViewModel;
+
+public class RootVM : NotifyPropertyChanged
+{
+ private const string ConfigPath = "./Userdata/Configurations/Configuration.json";
+
+ // Common
+ private UserControl currentUserControl = default!;
+ public UserControl CurrentUserControl
+ {
+ get => currentUserControl;
+ set => SetField(ref currentUserControl, value);
+ }
+
+ public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!;
+ public static string VersionShort => string.Format("{0}.{1:00}", Version.Major, Version.Minor);
+
+ // Models
+ public Preferences Preferences { get; init; }
+ public ObservableTable Table { get; set; } = new();
+
+ // ViewModels
+ public SplashVM? SplashVM { get; set; }
+ public WelcomeVM? WelcomeVM { get; set; }
+ public EditorVM? EditorVM { get; set; }
+ public SettingsVM? SettingsVM { get; set; }
+
+ public RootVM()
+ {
+ App.Logger.Info("Building DataContext");
+
+ Preferences = PreferencesReader.Import(ConfigPath);
+ if (Preferences.Theme is not null)
+ {
+ Application.Current.Resources.MergedDictionaries.Add(Preferences.Theme);
+ }
+
+ Initialize();
+ }
+
+ internal async void Initialize(bool hasFilePath = true)
+ {
+ CancellationTokenSource token = new();
+ SplashVM = new(token.Token);
+ CurrentUserControl = new Splash();
+
+ bool isTableValid = await Task.Run(() => BackgroundTask(hasFilePath));
+ token.Cancel();
+
+ // Switching to the Editor must be done in UI thread
+ // because an ICollectionView cannot be accessed from a different thread.
+ if (isTableValid)
+ {
+ EditorVM = new(this);
+ CurrentUserControl = new View.Editor();
+ return;
+ }
+
+ WelcomeVM = new(this);
+ CurrentUserControl = new Welcome();
+ }
+
+ internal bool BackgroundTask(bool hasFilePath = true)
+ {
+ if (!hasFilePath) { return true; }
+
+ Table = CsvFile.Import(Preferences.CsvPath, Preferences.HasHeader);
+ return Table.Headers.Count != 0;
+ }
+
+ public DelegateCommand Exit => new(
+ (e) =>
+ {
+ if (!EditorVM?.Edit.IsEditorDirty ?? true) { return; }
+
+ MessageBoxResult action = MessageBox.Show(
+ "Save changes before exiting?",
+ "RowsSharp",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Question
+ );
+
+ if (action == MessageBoxResult.Cancel)
+ {
+ e.Cancel = true;
+ }
+ else if (action == MessageBoxResult.Yes)
+ {
+ EditorVM?.Edit.Save.Execute(this);
+ }
+ }
+ );
+}
diff --git a/rowsSharp/ViewModel/Settings.cs b/rowsSharp/ViewModel/Settings.cs
new file mode 100644
index 0000000..5e79a08
--- /dev/null
+++ b/rowsSharp/ViewModel/Settings.cs
@@ -0,0 +1,17 @@
+ using System.Windows.Controls;
+
+namespace rowsSharp.ViewModel;
+
+public class SettingsVM : NotifyPropertyChanged
+{
+ private readonly RootVM rootVM;
+ private readonly UserControl previous;
+
+ public SettingsVM(RootVM rootViewModel, UserControl previousUserControl)
+ {
+ rootVM = rootViewModel;
+ previous = previousUserControl;
+ }
+
+ public DelegateCommand Return => new(() => rootVM.CurrentUserControl = previous);
+}
diff --git a/rowsSharp/ViewModel/Splash.cs b/rowsSharp/ViewModel/Splash.cs
new file mode 100644
index 0000000..895143d
--- /dev/null
+++ b/rowsSharp/ViewModel/Splash.cs
@@ -0,0 +1,34 @@
+using NLog;
+using NLog.Targets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace rowsSharp.ViewModel;
+
+public class SplashVM : NotifyPropertyChanged
+{
+ private const int RefreshLogDelay = 500; // ms
+
+ private string log = "";
+ public string Log
+ {
+ get => log;
+ set => SetField(ref log, value);
+ }
+
+ public SplashVM(CancellationToken token)
+ {
+ Task.Run(() => BackgroundTask(token), token);
+ }
+
+ private void BackgroundTask(CancellationToken token)
+ {
+ var target = LogManager.Configuration.FindTargetByName("Memory");
+ while (true)
+ {
+ Task.Delay(RefreshLogDelay, token).Wait(token);
+ Log = target.Logs[0];
+ if (token.IsCancellationRequested) { return; }
+ }
+ }
+}
diff --git a/rowsSharp/ViewModel/Welcome.cs b/rowsSharp/ViewModel/Welcome.cs
new file mode 100644
index 0000000..59e4b74
--- /dev/null
+++ b/rowsSharp/ViewModel/Welcome.cs
@@ -0,0 +1,43 @@
+using Microsoft.Win32;
+using rowsSharp.View;
+
+namespace rowsSharp.ViewModel;
+
+public class WelcomeVM : NotifyPropertyChanged
+{
+ private readonly RootVM rootVM;
+
+ public WelcomeVM(RootVM rootViewModel)
+ {
+ rootVM = rootViewModel;
+ }
+
+ private static string RequestFilePath()
+ {
+ OpenFileDialog dialog = new()
+ {
+ Filter = "Comma-seperated values (*.csv)|*.csv|All files (*.*)|*.*",
+ DefaultExt = "csv"
+ };
+
+ dialog.ShowDialog();
+
+ return dialog.FileName;
+ }
+
+ public DelegateCommand OpenFile => new(() =>
+ {
+ rootVM.Preferences.CsvPath = RequestFilePath();
+ rootVM.Initialize();
+ });
+
+ public DelegateCommand NewFile => new(
+ () => rootVM.Initialize(false)
+ );
+
+ public DelegateCommand OpenPreferences => new(() =>
+ {
+ rootVM.SettingsVM = new(rootVM, rootVM.CurrentUserControl);
+ rootVM.CurrentUserControl = new Settings();
+ });
+}
diff --git a/rowsSharp/rowsSharp.csproj b/rowsSharp/rowsSharp.csproj
index 9e569c4..cfc6855 100644
--- a/rowsSharp/rowsSharp.csproj
+++ b/rowsSharp/rowsSharp.csproj
@@ -8,46 +8,69 @@
False
false
True
+ 7.0
+ False
+
+ True
+
+
+
+ True
+
+
+
+
+ Never
+
+
+
-
-
-
-
-
-
-
-
-
+
+
-
+
+
$(DefaultXamlRuntime)
- PreserveNewest
+ Always
$(DefaultXamlRuntime)
- PreserveNewest
+ Always
$(DefaultXamlRuntime)
- PreserveNewest
+ Always
$(DefaultXamlRuntime)
- PreserveNewest
+ Always
+ Designer
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ Always
-
+
true
-
+
+
+
+
+
+
+ ..\..\ObservableTable\Publish\ObservableTable.dll
+
@@ -55,31 +78,30 @@
PreserveNewest
- PreserveNewest
+ Always
- PreserveNewest
-
-
- PreserveNewest
+ Always
-
- PreserveNewest
+
+ Always
-
- PreserveNewest
+
+ Always
-
- PreserveNewest
+
+ Always
- PreserveNewest
+ Always
-
+
$(DefaultXamlRuntime)
+ PreserveNewest
+ Designer
| | | |