diff --git a/.idea/.idea.Composition/.idea/.gitignore b/.idea/.idea.Composition/.idea/.gitignore
new file mode 100644
index 0000000..eeb5346
--- /dev/null
+++ b/.idea/.idea.Composition/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/projectSettingsUpdater.xml
+/.idea.Composition.iml
+/contentModel.xml
+/modules.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.Composition/.idea/encodings.xml b/.idea/.idea.Composition/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.Composition/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Composition/.idea/indexLayout.xml b/.idea/.idea.Composition/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.Composition/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Composition.sln b/Composition.sln
new file mode 100644
index 0000000..5315c81
--- /dev/null
+++ b/Composition.sln
@@ -0,0 +1,16 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Composition", "Composition\Composition.csproj", "{EDFAC570-EDA6-463F-83A3-E2E15D0E31E8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EDFAC570-EDA6-463F-83A3-E2E15D0E31E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EDFAC570-EDA6-463F-83A3-E2E15D0E31E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EDFAC570-EDA6-463F-83A3-E2E15D0E31E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EDFAC570-EDA6-463F-83A3-E2E15D0E31E8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/Composition.sln.DotSettings.user b/Composition.sln.DotSettings.user
new file mode 100644
index 0000000..1d79d31
--- /dev/null
+++ b/Composition.sln.DotSettings.user
@@ -0,0 +1,4 @@
+
+ True
+ True
+ True
\ No newline at end of file
diff --git a/Composition/Component/FileButtons.cs b/Composition/Component/FileButtons.cs
new file mode 100644
index 0000000..e387164
--- /dev/null
+++ b/Composition/Component/FileButtons.cs
@@ -0,0 +1,83 @@
+using System;
+using Composition.Model;
+using Gtk;
+
+namespace Composition.Component;
+
+public class FileButtons: HBox
+{
+ /// Event to trigger when a file path is chosen for exporting
+ public event Action? Export;
+ /// Event to trigger when a file path is chosen for importing
+ public event Action? Import;
+
+ private readonly Button _exportButton = new(Localization.Export_Button);
+ private readonly Button _importButton = new(Localization.Import_Fragment_Button);
+ private readonly Button _importFolderButton = new(Localization.Import_Folder_Button);
+
+ public FileButtons(Window window)
+ {
+ Add(_exportButton);
+ Add(_importButton);
+ Add(_importFolderButton);
+
+ const int buttonHeight = 50;
+ _exportButton.HeightRequest = buttonHeight;
+ _importButton.HeightRequest = buttonHeight;
+ _importFolderButton.HeightRequest = buttonHeight;
+
+ _exportButton.Image = new Image(Stock.Save, IconSize.Button);
+ _importButton.Image = new Image(Stock.File, IconSize.Button);
+ _importFolderButton.Image = new Image(Stock.Directory, IconSize.Button);
+
+ var exportDialog = new FileChooserDialog(Localization.Dialog_Export, window, FileChooserAction.Save, Localization.Dialog_Cancel, ResponseType.Cancel, Localization.Dialog_Export, ResponseType.Apply);
+ _exportButton.Clicked += (_, _) => exportDialog.Run();
+ exportDialog.Response += (_, response) =>
+ {
+ if (response.ResponseId == ResponseType.Apply)
+ {
+ string filePath = exportDialog.Filename;
+ if (filePath != null)
+ {
+ Export?.Invoke(filePath);
+ }
+ }
+ exportDialog.Hide();
+ };
+
+ var fileFilter = new FileFilter();
+ fileFilter.AddPattern("*.compose");
+ fileFilter.Name = Localization.File_Filter_Description;
+
+ var importDialog = new FileChooserDialog(Localization.Dialog_Import, window, FileChooserAction.Open, Localization.Dialog_Cancel, ResponseType.Cancel, Localization.Dialog_Import, ResponseType.Apply);
+ importDialog.AddFilter(fileFilter);
+ _importButton.Clicked += (_, _) => importDialog.Run();
+ importDialog.Response += (_, response) =>
+ {
+ if (response.ResponseId == ResponseType.Apply)
+ {
+ string filePath = importDialog.Filename;
+ if (filePath != null)
+ {
+ Import?.Invoke(filePath);
+ }
+ }
+ importDialog.Hide();
+ };
+
+ var importFolderDialog = new FileChooserDialog(Localization.Dialog_Import, window, FileChooserAction.SelectFolder, Localization.Dialog_Cancel, ResponseType.Cancel, Localization.Dialog_Import, ResponseType.Apply);
+ _importFolderButton.Clicked += (_, _) => importFolderDialog.Run();
+ importFolderDialog.Response += (_, response) =>
+ {
+ if (response.ResponseId == ResponseType.Apply)
+ {
+ string filePath = importFolderDialog.Filename;
+ if (filePath != null)
+ {
+ Import?.Invoke(filePath);
+ }
+ }
+ importFolderDialog.Hide();
+ };
+ }
+}
\ No newline at end of file
diff --git a/Composition/Component/KeysymView.cs b/Composition/Component/KeysymView.cs
new file mode 100644
index 0000000..4dc2ad0
--- /dev/null
+++ b/Composition/Component/KeysymView.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using Gtk;
+using Key = Gdk.Key;
+
+namespace Composition.Component;
+
+public class KeysymView: HBox
+{
+ private readonly Label _noneLabel = new();
+ public string NoneText
+ {
+ get => _noneLabel.Text;
+ set => _noneLabel.Text = value;
+ }
+
+ public KeysymView()
+ {
+
+ }
+
+ public void Update(IReadOnlyCollection keys)
+ {
+ Foreach(child => Remove(child));
+
+ string[] keyArray = keys.ToArray();
+ if (keyArray.Length > 0)
+ {
+ for (var i = 0; i < keyArray.Length; i++)
+ {
+ Label label = new(keyArray[i]);
+ label.Visible = true;
+ label.StyleContext.AddClass("sequence-key");
+
+ // Separator
+ if (i != 0)
+ {
+ Label seperator = new(" + ");
+ seperator.Visible = true;
+ seperator.StyleContext.AddClass("sequence-seperator");
+ Add(seperator);
+ }
+
+ // Add the label to the box
+ Add(label);
+ }
+ }
+ else
+ {
+ Label label = new("No keys chosen\nPress a key to add it");
+ label.Visible = true;
+ label.StyleContext.AddClass("sequence-seperator");
+ Add(label);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Composition/Component/SequenceInput.cs b/Composition/Component/SequenceInput.cs
new file mode 100644
index 0000000..22bd91e
--- /dev/null
+++ b/Composition/Component/SequenceInput.cs
@@ -0,0 +1,80 @@
+using System;
+using Composition.Model;
+using Gtk;
+using Key = Gdk.Key;
+
+namespace Composition.Component;
+
+public class SequenceInput: HBox
+{
+ readonly SequenceBuilder builder;
+ private readonly Button _clear = new();
+ private readonly Button _keys = new();
+ private readonly KeysymView _keysymsView = new();
+ private readonly Entry _character = new();
+ private readonly Button _complete = new();
+
+ public event Action? CompletedEvent;
+
+ public SequenceInput(SequenceBuilder builder)
+ {
+ this.builder = builder;
+
+ Add(_clear);
+ Add(_keys);
+ _keys.Add(_keysymsView);
+ Add(_character);
+ Add(_complete);
+
+ // Clear button
+ _clear.Image = new Image(Stock.Clear, IconSize.Button);
+ _clear.AlwaysShowImage = true;
+ _clear.Clicked += (_, _) => builder.ClearKeys();
+ // Apply button
+ _complete.Image = new Image(Stock.Apply, IconSize.Button);
+ _complete.AlwaysShowImage = true;
+ _complete.Clicked += (_, _) => BuildSequence();
+ // Key input
+ _keys.KeyPressEvent += (_, e) => e.RetVal = true; // Prevent event propagation to the window
+ _keys.KeyReleaseEvent += KeyInput;
+ _keysymsView.NoneText = "No keys chosen\nPress a key to add it";
+ // Character output
+ _character.Alignment = 0.5f; // Center
+ _character.StyleContext.AddClass(Resources.CSS_sequence_character);
+
+ this.builder.OnChanged += _ => Update();
+ Update();
+ }
+
+ public void KeyInput(object _, KeyReleaseEventArgs e)
+ {
+ // Completes the sequence when enter is pressed
+ if (e.Event.Key == Key.Return)
+ {
+ _complete.Click();
+ }
+ // Cancel input
+ else if (e.Event.Key == Key.Escape)
+ {
+ _clear.Click();
+ }
+ // Appends the key to the sequence
+ else
+ {
+ builder.AddKey(e.Event.Key);
+ }
+ }
+
+ private void BuildSequence()
+ {
+ Sequence sequence = builder.Build()!;
+ CompletedEvent?.Invoke(sequence);
+ builder.ClearKeys();
+ }
+
+ private void Update()
+ {
+ _keysymsView.Update(builder.Keys);
+ _character.Text = builder.Character?.ToString() ?? "";
+ }
+}
\ No newline at end of file
diff --git a/Composition/Component/SequenceTreeView.cs b/Composition/Component/SequenceTreeView.cs
new file mode 100644
index 0000000..4033b11
--- /dev/null
+++ b/Composition/Component/SequenceTreeView.cs
@@ -0,0 +1,43 @@
+using System;
+using Composition.Model;
+using Gtk;
+
+namespace Composition.Component;
+
+public class SequenceTreeView: TreeView
+{
+ public SequenceTreeView(): base()
+ {
+ TreeViewColumn keysymsColumn = new();
+ keysymsColumn.Title = "Sequence";
+ CellRendererText keysymsCell = new();
+ keysymsColumn.PackStart(keysymsCell, true);
+
+ TreeViewColumn characterColumn = new();
+ characterColumn.Title = "Result";
+ CellRendererText characterCell = new();
+ characterCell.Style = Pango.Style.Oblique;
+ characterColumn.PackStart(characterCell, true);
+
+ TreeViewColumn toggleColumn = new();
+ toggleColumn.Title = "Enabled";
+ CellRendererToggle toggleCell = new();
+ toggleColumn.PackStart(toggleCell, true);
+
+ AppendColumn(keysymsColumn);
+ AppendColumn(characterColumn);
+ AppendColumn(toggleColumn);
+
+ keysymsColumn.AddAttribute(keysymsCell, "text", 0);
+ characterColumn.AddAttribute(characterCell, "text", 1);
+ toggleColumn.AddAttribute(toggleCell, "active", 3);
+
+ toggleCell.Toggled += (widget, path) =>
+ {
+ TreeIter iter;
+ Model.GetIter(out iter, new TreePath(path.Path));
+ bool enabled = (bool) Model.GetValue(iter, 3);
+ Model.SetValue(iter, 3, !enabled);
+ };
+ }
+}
\ No newline at end of file
diff --git a/Composition/Component/SequenceView.cs b/Composition/Component/SequenceView.cs
new file mode 100644
index 0000000..cb47fb5
--- /dev/null
+++ b/Composition/Component/SequenceView.cs
@@ -0,0 +1,35 @@
+using Composition.Model;
+using Gtk;
+
+namespace Composition.Component;
+
+public class SequenceView: HBox
+{
+ private Sequence _sequence;
+ public Sequence Sequence
+ {
+ get => _sequence;
+ set
+ {
+ _sequence = value;
+ Update();
+ }
+ }
+
+ private readonly KeysymView _keysymView = new() { Visible = true };
+ private readonly Label _separator = new() { Visible = true, Text = "→" };
+ private readonly Label _character = new() { Visible = true };
+
+ public SequenceView()
+ {
+ Add(_keysymView);
+ Add(_separator);
+ Add(_character);
+ }
+
+ private void Update()
+ {
+ _keysymView.Update(_sequence.Keys);
+ _character.Text = _sequence.Character.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Composition/Composition.csproj b/Composition/Composition.csproj
new file mode 100644
index 0000000..7c7506b
--- /dev/null
+++ b/Composition/Composition.csproj
@@ -0,0 +1,41 @@
+
+
+
+ WinExe
+ net6.0
+ enable
+
+
+
+
+
+ %(Filename)%(Extension)
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ ResXFileCodeGenerator
+ Localization.Designer.cs
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ True
+ Localization.resx
+
+
+
+
diff --git a/Composition/Composition.csproj.DotSettings.user b/Composition/Composition.csproj.DotSettings.user
new file mode 100644
index 0000000..2f2b44b
--- /dev/null
+++ b/Composition/Composition.csproj.DotSettings.user
@@ -0,0 +1,2 @@
+
+ EDFAC570-EDA6-463F-83A3-E2E15D0E31E8/f:Localization.resx
\ No newline at end of file
diff --git a/Composition/FileParser.cs b/Composition/FileParser.cs
new file mode 100644
index 0000000..fe929e1
--- /dev/null
+++ b/Composition/FileParser.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Composition.Model;
+
+namespace Composition;
+
+public static class FileParser
+{
+ public static List Parse(string filePath)
+ {
+ var sequences = new List();
+ var lines = File.ReadAllLines(filePath);
+ foreach (string line in lines)
+ {
+ Sequence? sequence = ParseLine(line);
+ if (sequence != null)
+ {
+ sequences.Add(sequence);
+ }
+ }
+ return sequences;
+ }
+
+ private static readonly Regex _whiteSpaceRegex = new Regex("\\s+");
+ private static readonly Regex _sequenceRegex = new Regex("\\s+(?<[^>]+>(\\s+<[^>]+>)*)\\s*:\\s*\"(?[^\"]+)\"(\\s+.+)?\\s*(#.+)?");
+ private static Sequence? ParseLine(string line)
+ {
+ Match match = _sequenceRegex.Match(line);
+
+ if (match.Success)
+ {
+ string[] keysyms = _whiteSpaceRegex
+ .Split(match.Groups["keysyms"].Value)
+ .Select(keysym => keysym[1..^1])
+ .ToArray();
+ string result = match.Groups["result"].Value;
+
+ var sequence = new Sequence(keysyms, result);
+ return sequence;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Composition/Localization.Designer.cs b/Composition/Localization.Designer.cs
new file mode 100644
index 0000000..fe25b62
--- /dev/null
+++ b/Composition/Localization.Designer.cs
@@ -0,0 +1,102 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Composition {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Localization {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Localization() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Composition.Localization", typeof(Localization).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Load_File_Message {
+ get {
+ return ResourceManager.GetString("Load_File_Message", resourceCulture);
+ }
+ }
+
+ internal static string Skip_File_Message {
+ get {
+ return ResourceManager.GetString("Skip_File_Message", resourceCulture);
+ }
+ }
+
+ internal static string Import_Fragment_Button {
+ get {
+ return ResourceManager.GetString("Import_Fragment_Button", resourceCulture);
+ }
+ }
+
+ internal static string Export_Button {
+ get {
+ return ResourceManager.GetString("Export_Button", resourceCulture);
+ }
+ }
+
+ internal static string Import_Folder_Button {
+ get {
+ return ResourceManager.GetString("Import_Folder_Button", resourceCulture);
+ }
+ }
+
+ internal static string Dialog_Import {
+ get {
+ return ResourceManager.GetString("Dialog_Import", resourceCulture);
+ }
+ }
+
+ internal static string Dialog_Cancel {
+ get {
+ return ResourceManager.GetString("Dialog_Cancel", resourceCulture);
+ }
+ }
+
+ internal static string File_Filter_Description {
+ get {
+ return ResourceManager.GetString("File_Filter_Description", resourceCulture);
+ }
+ }
+
+ internal static string Dialog_Export {
+ get {
+ return ResourceManager.GetString("Dialog_Export", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Composition/Localization.resx b/Composition/Localization.resx
new file mode 100644
index 0000000..0312d63
--- /dev/null
+++ b/Composition/Localization.resx
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Loading {0} {1}...
+
+
+ Skipping {0} {1}...
+
+
+ Import compose fragment
+
+
+ Export compose file
+
+
+ Import compose fragment folder
+
+
+ Import
+
+
+ Cancel
+
+
+ Compose fragment files
+
+
+ Export
+
+
\ No newline at end of file
diff --git a/Composition/Model/Sequence.cs b/Composition/Model/Sequence.cs
new file mode 100644
index 0000000..304beef
--- /dev/null
+++ b/Composition/Model/Sequence.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Composition.Model;
+
+public class Sequence
+{
+ private static readonly Regex _keyRegex = new Regex("^KEY_");
+ public static string GdkToKeysym(Gdk.Key key) => _keyRegex.Replace(key.ToString(), "");
+
+ public string[] Keys { get; }
+ public string Character { get; }
+
+ public Sequence(IReadOnlyList keys, string character)
+ {
+ Keys = keys.ToArray();
+ Character = character;
+ }
+
+ public override string ToString() => Serialize(0);
+
+ public string Serialize(int leftMinWidth)
+ {
+ string keys = string.Join(" ", Keys.Select(key => $"<{key}>"));
+ string start = $" {keys} ";
+ string end = $": \"{Character}\"";
+ return start.PadRight(leftMinWidth, ' ') + end;
+ }
+
+ public int GetLeftMinWidth()
+ {
+ int length = "".Length;
+ foreach (string key in Keys)
+ {
+ // +3 for the < and > and the space
+ length += key.Length + 3;
+ }
+ length += 1; // for the space after the keys
+ return length;
+ }
+}
\ No newline at end of file
diff --git a/Composition/Model/SequenceBuilder.cs b/Composition/Model/SequenceBuilder.cs
new file mode 100644
index 0000000..698cb84
--- /dev/null
+++ b/Composition/Model/SequenceBuilder.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+
+namespace Composition.Model;
+
+public class SequenceBuilder
+{
+ private readonly List _keys = new();
+
+ private string? _result;
+ public string? Character
+ {
+ get => _result;
+ set {
+ _result = value;
+ OnChanged?.Invoke(this);
+ }
+ }
+
+ public event Action? OnChanged;
+
+ public void AddKey(Gdk.Key key)
+ {
+ _keys.Add(key.ToString());
+ OnChanged?.Invoke(this);
+ }
+
+ public List Keys => _keys;
+
+ public void ClearKeys()
+ {
+ _keys.Clear();
+ OnChanged?.Invoke(this);
+ }
+
+ public Sequence? Build()
+ {
+ if (_result == null || _keys.Count == 0) return null;
+ return new Sequence(_keys, _result);
+ }
+}
\ No newline at end of file
diff --git a/Composition/Model/SequenceTreeStore.cs b/Composition/Model/SequenceTreeStore.cs
new file mode 100644
index 0000000..10a5a41
--- /dev/null
+++ b/Composition/Model/SequenceTreeStore.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Gtk;
+using Path = System.IO.Path;
+
+namespace Composition.Model;
+
+public class SequenceTreeStore: TreeStore
+{
+ private readonly Dictionary _filePathToIter = new();
+
+ public SequenceTreeStore(): base(typeof(string), typeof(string), typeof(Sequence), typeof(bool))
+ {
+
+ }
+
+ public bool UnloadFile(string filePath)
+ {
+ bool containsKey = _filePathToIter.ContainsKey(filePath);
+ if (containsKey)
+ {
+ TreeIter iter = _filePathToIter[filePath];
+ Remove(ref iter);
+ }
+ return containsKey;
+ }
+
+ public void LoadFile(string filePath)
+ {
+ bool isFolder(string path) => File.GetAttributes(path).HasFlag(FileAttributes.Directory);
+
+ Console.WriteLine(Localization.Load_File_Message, (isFolder(filePath) ? "folder" : "file"), filePath);
+ if (isFolder(filePath))
+ {
+ var files = Directory.GetFiles(filePath);
+ foreach (string file in files)
+ {
+ if (file.EndsWith(".compose") || isFolder(file))
+ {
+ LoadFile(file);
+ }
+ else
+ {
+ Console.WriteLine(Localization.Skip_File_Message, (isFolder(file) ? "folder" : "file"), file);
+ }
+ }
+ }
+ else
+ {
+ UnloadFile(filePath);
+
+ List sequences = FileParser.Parse(filePath);
+
+ string name = Path.GetFileName(filePath);
+ name = Regex.Replace(name, @"\.compose$", "", RegexOptions.IgnoreCase);
+ TreeIter fileIter = AppendValues(name, "", true);
+ _filePathToIter[filePath] = fileIter;
+
+ foreach (Sequence sequence in sequences)
+ {
+ string keysyms = string
+ .Join(" + ", sequence.Keys.Select(key => key.ToString()))
+ .Replace("Multi_key", "⎄");
+ string character = sequence.Character;
+ TreeIter sequenceIter = AppendValues(fileIter, keysyms, character, sequence, true);
+ }
+ }
+ }
+
+ public Dictionary> GetEnabledSequences()
+ {
+ var sequences = new Dictionary>();
+ TreeIter iter;
+ if (GetIterFirst(out iter))
+ {
+ do
+ {
+ string filePath = (string) GetValue(iter, 0);
+ bool fileEnabled = (bool) GetValue(iter, 3);
+ sequences[filePath] = new List();
+ TreeIter childIter;
+ if (fileEnabled && IterChildren(out childIter, iter))
+ {
+ do
+ {
+ Sequence sequence = (Sequence) GetValue(childIter, 2);
+ bool sequenceEnabled = (bool) GetValue(childIter, 3);
+ if (sequenceEnabled)
+ {
+ sequences[filePath].Add(sequence);
+ }
+ } while (IterNext(ref childIter));
+ }
+ } while (IterNext(ref iter));
+ }
+ return sequences;
+ }
+
+ public void SaveFile(string filePath)
+ {
+ string content = Serialize();
+ File.WriteAllText(filePath, content);
+ }
+
+ public string Serialize()
+ {
+ Dictionary> sequences = GetEnabledSequences();
+
+ var sb = new StringBuilder();
+ sb.AppendLine("# XCompose file generated by Composition");
+ sb.AppendLine("include \"%L\"");
+ sb.AppendLine("");
+
+ foreach (string filePath in sequences.Keys)
+ {
+ if (sequences[filePath].Count > 0)
+ {
+ string fileName = Path.GetFileName(filePath);
+ int leftMinWidth = sequences[filePath].Max(sequence => sequence.GetLeftMinWidth());
+ sb.AppendLine($"# {fileName}");
+ foreach (Sequence sequence in sequences[filePath])
+ {
+ sb.AppendLine(sequence.Serialize(leftMinWidth));
+ }
+ sb.AppendLine("");
+ }
+ }
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Composition/Program.cs b/Composition/Program.cs
new file mode 100644
index 0000000..d01176f
--- /dev/null
+++ b/Composition/Program.cs
@@ -0,0 +1,58 @@
+using Composition.Component;
+using Composition.Model;
+using Gdk;
+using Gtk;
+using Window = Gtk.Window;
+
+namespace Composition;
+
+class Program
+{
+ public static void Main(string[] args)
+ {
+ Application.Init();
+
+ var cssProvider = new CssProvider();
+ cssProvider.LoadFromData(Resources.Style);
+ StyleContext.AddProviderForScreen(Screen.Default, cssProvider, 800);
+
+ var window = new Window(Resources.Name);
+ window.SetSizeRequest(600, 800);
+ window.IconName = "input-keyboard";
+ window.DeleteEvent += (_, _) => Application.Quit();
+
+ var store = new SequenceTreeStore();
+
+ var mainBox = new VBox();
+ window.Add(mainBox);
+
+ var buttonBox = new FileButtons(window);
+ buttonBox.Import += filePath => store.LoadFile(filePath);
+ buttonBox.Export += filePath => store.SaveFile(filePath);
+ mainBox.PackStart(buttonBox, false, false, 0);
+
+ var scrollable = new ScrolledWindow();
+ mainBox.Add(scrollable);
+
+ var tree = new SequenceTreeView { Model = store };
+ scrollable.Add(tree);
+
+ // var box = new VBox();
+ // window.Add(box);
+ // var builder = new SequenceBuilder();
+ // builder.Character = "a";
+ //
+ // var input = new SequenceInput(builder);
+ // input.HeightRequest = 50;
+ // input.CompletedEvent += sequence =>
+ // {
+ // var view = new SequenceView { Sequence = sequence };
+ // view.Visible = true;
+ // box.Add(view);
+ // };
+ // box.Add(input);
+
+ window.ShowAll();
+ Application.Run();
+ }
+}
\ No newline at end of file
diff --git a/Composition/Resources.Designer.cs b/Composition/Resources.Designer.cs
new file mode 100644
index 0000000..6242746
--- /dev/null
+++ b/Composition/Resources.Designer.cs
@@ -0,0 +1,78 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Composition {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Composition.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Name {
+ get {
+ return ResourceManager.GetString("Name", resourceCulture);
+ }
+ }
+
+ internal static string CSS_sequence_character {
+ get {
+ return ResourceManager.GetString("CSS_sequence_character", resourceCulture);
+ }
+ }
+
+ internal static string CSS_sequence_seperator {
+ get {
+ return ResourceManager.GetString("CSS_sequence-seperator", resourceCulture);
+ }
+ }
+
+ internal static string CSS_sequence_key {
+ get {
+ return ResourceManager.GetString("CSS_sequence-key", resourceCulture);
+ }
+ }
+
+ internal static string Style {
+ get {
+ return ResourceManager.GetString("Style", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Composition/Resources.resx b/Composition/Resources.resx
new file mode 100644
index 0000000..c2c1d83
--- /dev/null
+++ b/Composition/Resources.resx
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Composition
+
+
+ sequence-character
+
+
+ sequence-seperator
+
+
+ sequence-key
+
+
+
+ .sequence-character {
+ font-weight: bolder;
+ font-size: 1.5em;
+ }
+
+ .sequence-seperator {
+ opacity: 0.5;
+ }
+
+ .sequence-key {
+ font-weight: bold;
+ }
+
+
+
\ No newline at end of file
diff --git a/Composition/style.css b/Composition/style.css
new file mode 100644
index 0000000..dbf200e
--- /dev/null
+++ b/Composition/style.css
@@ -0,0 +1,3 @@
+.sequence-input-character {
+ font-weight: bolder;
+}
\ No newline at end of file
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..e734b06
Binary files /dev/null and b/screenshot.png differ