diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 08fe633..4d633ee 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -37,6 +37,8 @@ public sealed class DataContext : DbContext modelBuilder.Entity
().HasIndex(n => n.Name); modelBuilder.Entity
().HasIndex(n => n.Depth); modelBuilder.Entity
().HasIndex(n => n.DataKey); + // modelBuilder.Entity
().Navigation(n => n.Parent).AutoInclude(); // Cycle-reference, does not work, though. + modelBuilder.Entity
().Navigation(n => n.TextElements).AutoInclude(); #endregion @@ -44,6 +46,8 @@ public sealed class DataContext : DbContext modelBuilder.Entity().HasIndex(n => n.Id); modelBuilder.Entity().HasIndex(n => n.Code); + modelBuilder.Entity().HasIndex(n => n.Name); + modelBuilder.Entity().Navigation(n => n.Section).AutoInclude(); #endregion @@ -52,6 +56,7 @@ public sealed class DataContext : DbContext modelBuilder.Entity().HasIndex(n => n.Id); modelBuilder.Entity().HasIndex(n => n.Culture); modelBuilder.Entity().HasIndex(n => n.Text); + modelBuilder.Entity().Navigation(n => n.TextElement).AutoInclude(); #endregion } diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index 2135ffe..43fe3c0 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -7,9 +7,11 @@ public sealed class TextElement [Key] public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; public Section Section { get; set; } - public List Translations { get; set; } + public List Translations { get; set; } = new(); } \ No newline at end of file diff --git a/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.Designer.cs b/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.Designer.cs new file mode 100644 index 0000000..f3b9a47 --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.Designer.cs @@ -0,0 +1,201 @@ +// +using System; +using DataModel.Database.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DataModel.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20220710172935_202207AddTextElementName")] + partial class _202207AddTextElementName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DataKey"); + + b.HasIndex("Depth"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("DataModel.Database.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoolValue") + .HasColumnType("INTEGER"); + + b.Property("GuidValue") + .HasColumnType("TEXT"); + + b.Property("IntegerValue") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BoolValue"); + + b.HasIndex("GuidValue"); + + b.HasIndex("Id"); + + b.HasIndex("IntegerValue"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("TextValue"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("SectionId"); + + b.ToTable("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Culture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextElementId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Culture"); + + b.HasIndex("Id"); + + b.HasIndex("Text"); + + b.HasIndex("TextElementId"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.HasOne("DataModel.Database.Section", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.HasOne("DataModel.Database.Section", "Section") + .WithMany("TextElements") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.HasOne("DataModel.Database.TextElement", "TextElement") + .WithMany("Translations") + .HasForeignKey("TextElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TextElement"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Navigation("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Navigation("Translations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.cs b/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.cs new file mode 100644 index 0000000..f21f4ea --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220710172935_202207AddTextElementName.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataModel.Migrations +{ + public partial class _202207AddTextElementName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Name", + table: "TextElements", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_TextElements_Name", + table: "TextElements", + column: "Name"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_TextElements_Name", + table: "TextElements"); + + migrationBuilder.DropColumn( + name: "Name", + table: "TextElements"); + } + } +} diff --git a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs index 6a285f4..616d211 100644 --- a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs +++ b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs @@ -103,6 +103,10 @@ namespace DataModel.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("SectionId") .HasColumnType("INTEGER"); @@ -112,6 +116,8 @@ namespace DataModel.Migrations b.HasIndex("Id"); + b.HasIndex("Name"); + b.HasIndex("SectionId"); b.ToTable("TextElements"); diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index ed1388d..2b87b24 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -11,6 +11,10 @@ public static class Setup private const string DB_READ_WRITE_MODE = "ReadWrite"; private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate"; + private static string usedDataFile = string.Empty; + + public static string DataFile => Setup.usedDataFile; + /// /// Tries to migrate the data file. /// @@ -30,7 +34,8 @@ public static class Setup /// public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true) { - serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)}"), ServiceLifetime.Transient); + Setup.usedDataFile = path2DataFile; + serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient); } /// diff --git a/I18N Commander/Processor/ExtensionsError.cs b/I18N Commander/Processor/ExtensionsError.cs new file mode 100644 index 0000000..ed058c9 --- /dev/null +++ b/I18N Commander/Processor/ExtensionsError.cs @@ -0,0 +1,34 @@ +using System.Text; +using DataModel; +using Microsoft.Data.Sqlite; + +namespace Processor; + +public static class ExtensionsError +{ + public static ProcessorResult ToProcessorResult(this Exception exception) where TResult : class + { + if(exception.InnerException is SqliteException sqliteException) + if (sqliteException.SqliteErrorCode is 14) + { + var blockingProcesses = SystemUtil.WhoIsLocking(Setup.DataFile); + var sb = new StringBuilder(); + sb.AppendLine($"The data file is used by the {blockingProcesses.Count} other process(es) listed below:"); + foreach (var blockingProcess in blockingProcesses) + sb.AppendLine($"- {blockingProcess.ProcessName} (id={blockingProcess.Id})"); + + // Is there only one process and it is this one? + if (blockingProcesses.Count == 1 && blockingProcesses.First().ProcessName == "I18N Commander") + { + sb.AppendLine(); + sb.AppendLine("Hint: Is the ransomware protection enabled in your Windows system? If so, please make sure that the I18N Commander has write permission."); + } + + return new ProcessorResult(null, false, sb.ToString()); + } + else + return new ProcessorResult(null, false, $"A database error occurred: '{sqliteException.Message}'"); + + return new ProcessorResult(null, false, $"A database error occurred: '{exception.Message}'"); + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/GetBlockingProcesses.cs b/I18N Commander/Processor/GetBlockingProcesses.cs new file mode 100644 index 0000000..d4a29a9 --- /dev/null +++ b/I18N Commander/Processor/GetBlockingProcesses.cs @@ -0,0 +1,135 @@ +namespace Processor; + +// +// Source: https://stackoverflow.com/a/20623311/2258393 +// + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +public static class SystemUtil +{ + [StructLayout(LayoutKind.Sequential)] + private struct RM_UNIQUE_PROCESS + { + public readonly int dwProcessId; + private readonly System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; + } + + private const int RM_REBOOT_REASON_NONE = 0; + private const int CCH_RM_MAX_APP_NAME = 255; + private const int CCH_RM_MAX_SVC_NAME = 63; + + private enum RM_APP_TYPE + { + RM_UNKNOWN_APP = 0, + RM_MAIN_WINDOW = 1, + RM_OTHER_WINDOW = 2, + RM_SERVICE = 3, + RM_EXPLORER = 4, + RM_CONSOLE = 5, + RM_CRITICAL = 1000 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct RM_PROCESS_INFO + { + public readonly RM_UNIQUE_PROCESS Process; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] + private readonly string strAppName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] + private readonly string strServiceShortName; + + private readonly RM_APP_TYPE ApplicationType; + private readonly uint AppStatus; + private readonly uint TSSessionId; + + [MarshalAs(UnmanagedType.Bool)] + private readonly bool bRestartable; + } + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] + static extern int RmRegisterResources(uint pSessionHandle, UInt32 nFiles, string[] rgsFilenames, UInt32 nApplications, [In] RM_UNIQUE_PROCESS[] rgApplications, UInt32 nServices, string[] rgsServiceNames); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] + static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey); + + [DllImport("rstrtmgr.dll")] + static extern int RmEndSession(uint pSessionHandle); + + [DllImport("rstrtmgr.dll")] + static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons); + + /// + /// Find out what process(es) have a lock on the specified file. + /// + /// Path of the file. + /// Processes locking the file + /// See also: + /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx + /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing) + /// + public static List WhoIsLocking(string path) + { + var key = Guid.NewGuid().ToString(); + var processes = new List(); + + var res = RmStartSession(out var handle, 0, key); + if (res != 0) + return processes; + + try + { + const int ERROR_MORE_DATA = 234; + uint pnProcInfoNeeded = 0, pnProcInfo = 0, lpdwRebootReasons = RM_REBOOT_REASON_NONE; + var resources = new[] { path }; // Just checking on one resource. + res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null!, 0, null!); + + if (res != 0) + return processes; + + //Note: there's a race condition here -- the first call to RmGetList() returns + // the total number of process. However, when we call RmGetList() again to get + // the actual processes this number may have increased. + res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null!, ref lpdwRebootReasons); + if (res == ERROR_MORE_DATA) + { + // Create an array to store the process results + var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; + pnProcInfo = pnProcInfoNeeded; + + // Get the list + res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); + if (res == 0) + { + // Enumerate all of the results and add them to the list to be returned + for (var i = 0; i < pnProcInfo; i++) + { + try + { + processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId)); + } + // catch the error -- in case the process is no longer running + catch (ArgumentException) + { + } + } + } + else + return processes; + } + else if (res != 0) + return processes; + } + finally + { + RmEndSession(handle); + } + + return processes; + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/ProcessorMeta.cs b/I18N Commander/Processor/ProcessorMeta.cs new file mode 100644 index 0000000..bf57c59 --- /dev/null +++ b/I18N Commander/Processor/ProcessorMeta.cs @@ -0,0 +1,18 @@ +namespace Processor; + +public static class ProcessorMeta +{ + private static IServiceProvider? SERVICE_PROVIDER; + + public static IServiceProvider ServiceProvider + { + get => ProcessorMeta.SERVICE_PROVIDER!; + set + { + if(ProcessorMeta.SERVICE_PROVIDER is not null) + return; + + ProcessorMeta.SERVICE_PROVIDER = value; + } + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/ProcessorResult.cs b/I18N Commander/Processor/ProcessorResult.cs new file mode 100644 index 0000000..5678787 --- /dev/null +++ b/I18N Commander/Processor/ProcessorResult.cs @@ -0,0 +1,3 @@ +namespace Processor; + +public readonly record struct ProcessorResult(TResult? Result, bool Successful = true, string ErrorMessage = ""); \ No newline at end of file diff --git a/I18N Commander/Processor/SectionProcessor.cs b/I18N Commander/Processor/SectionProcessor.cs index a008a78..f66275b 100644 --- a/I18N Commander/Processor/SectionProcessor.cs +++ b/I18N Commander/Processor/SectionProcessor.cs @@ -1,6 +1,7 @@ using DataModel.Database; using DataModel.Database.Common; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Processor; @@ -9,16 +10,24 @@ public static class SectionProcessor /// /// Load one layer of the tree by using the specified depth: /// - public static IAsyncEnumerable
LoadLayer(DataContext db, int depth) + public static async Task> LoadLayer(int depth) { - return db.Sections.Where(n => n.Depth == depth).OrderBy(n => n.Id).AsAsyncEnumerable(); + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + var sections = await db.Sections.Where(n => n.Depth == depth).OrderBy(n => n.Id).ToListAsync(); + + // Ensure, that the database loaded the section's parent: + foreach (var section in sections) + await db.Entry(section).Reference(n => n.Parent).LoadAsync(); + + return sections; } /// /// Determine how deep the tree is. /// - public static async ValueTask GetDepth(DataContext db) + public static async ValueTask GetDepth() { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); if(!await db.Sections.AnyAsync()) { return 0; @@ -30,24 +39,15 @@ public static class SectionProcessor /// /// Compute the new sections key and its depth, then store the section in the database. /// - public static async Task
AddSection(DataContext db, string text, string? parentKey) + public static async Task> AddSection(string text, string? parentKey) { - // Remove any whitespaces from the section name, regardless of how many e.g. spaces the user typed: - var key = string.Join('_', text.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToUpperInvariant(); - - // Check, if this key already exists: - if (await db.Sections.AnyAsync(n => n.DataKey == key)) - { - var rng = new Random(); - while (await db.Sections.AnyAsync(n => n.DataKey == key)) - { - // Add a random number to the end of the key: - key += $"_{rng.Next(1, 10_000)}"; - } - } + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + // Generate the key: + var key = await Utils.GenerateCode(text, db.Sections, (n, key) => n.DataKey == key); + // In the case, when the user adds a section to the root, handle the insert differently: - if (string.IsNullOrEmpty(parentKey)) + if (string.IsNullOrWhiteSpace(parentKey)) { var rootSection = new Section { @@ -58,9 +58,16 @@ public static class SectionProcessor TextElements = new(), }; - db.Sections.Add(rootSection); - await db.SaveChangesAsync(); - return rootSection; + try + { + db.Sections.Add(rootSection); + await db.SaveChangesAsync(); + return new ProcessorResult
(rootSection); + } + catch (Exception e) + { + return e.ToProcessorResult
(); + } } // Read the parent from the database: @@ -77,30 +84,45 @@ public static class SectionProcessor TextElements = new(), Depth = parent.Depth + 1, }; - - db.Sections.Add(section); - await db.SaveChangesAsync(); - return section; + + try + { + await db.Sections.AddAsync(section); + await db.SaveChangesAsync(); + return new ProcessorResult
(section); + } + catch (Exception e) + { + return e.ToProcessorResult
(); + } } - public static async Task RemoveSection(DataContext db, string selectedKey) + public static async Task RemoveSection(string selectedKey) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + await SectionProcessor.RemoveOneSectionAndItsChildren(db, selectedKey); + await db.SaveChangesAsync(); + } + + private static async Task RemoveOneSectionAndItsChildren(DataContext db, string selectedKey) { // Remove the section from the database: var section2Delete = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == selectedKey); if (section2Delete is null) throw new ArgumentException($"The section with key {selectedKey} does not exist in the database."); - + // Next, remove all children of the section, and the children's children, etc.: var children = await db.Sections.Where(n => n.Parent == section2Delete).ToListAsync(); foreach (var child in children) - await RemoveSection(db, child.DataKey); + await SectionProcessor.RemoveOneSectionAndItsChildren(db, child.DataKey); db.Sections.Remove(section2Delete); - await db.SaveChangesAsync(); } - - public static async Task NumberChildren(DataContext db, string selectedKey) + + public static async Task NumberChildren(string selectedKey) { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + // Read the section from the database: var section = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == selectedKey); if (section is null) @@ -109,8 +131,10 @@ public static class SectionProcessor return await db.Sections.CountAsync(n => n.Parent == section); } - public static async Task
RenameSection(DataContext db, string selectedNodeKey, string alteredName) + public static async Task> RenameSection(string selectedNodeKey, string alteredName) { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + // Read the section from the database: var section = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == selectedNodeKey); if (section is null) @@ -132,7 +156,46 @@ public static class SectionProcessor section.Name = alteredName; section.DataKey = newKey; - await db.SaveChangesAsync(); + + try + { + await db.SaveChangesAsync(); + return new ProcessorResult
(section); + } + catch (Exception e) + { + return e.ToProcessorResult
(); + } + } + + public static async Task
GetSection(string sectionKey) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + var section = await db.Sections.FirstAsync(n => n.DataKey == sectionKey); + await db.Entry(section).Reference(n => n.Parent).LoadAsync(); + return section; } + + public static async Task GetSectionPath(string sectionKey) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + var section = await db.Sections.FirstAsync(n => n.DataKey == sectionKey); + + // Ensure, that the database loaded the section's parent: + await db.Entry(section).Reference(n => n.Parent).LoadAsync(); + + var path = section.Name; + while (section.Parent != null) + { + section = await db.Sections.FirstAsync(n => n.DataKey == section.Parent.DataKey); + + // Ensure, that the database loaded the section's parent: + await db.Entry(section).Reference(n => n.Parent).LoadAsync(); + + path = $"{section.Name}/{path}"; + } + + return $"Section's path: {path}"; + } } \ No newline at end of file diff --git a/I18N Commander/Processor/TextElementProcessor.cs b/I18N Commander/Processor/TextElementProcessor.cs new file mode 100644 index 0000000..7f30adc --- /dev/null +++ b/I18N Commander/Processor/TextElementProcessor.cs @@ -0,0 +1,113 @@ +using DataModel.Database; +using DataModel.Database.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Processor; + +public static class TextElementProcessor +{ + // Load all text elements for one particular section: + public static async Task> GetTextElements(Section section, string filterTerm) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + + if(string.IsNullOrWhiteSpace(filterTerm)) + return await db.TextElements.Where(n => n.Section == section).OrderBy(n => n.Name).ThenBy(n => n.Id).ToListAsync(); + else + return await db.TextElements.Where(n => n.Section == section && n.Name.Contains(filterTerm)).OrderBy(n => n.Name).ThenBy(n => n.Id).ToListAsync(); + } + + // Load one text element by id: + public static async Task LoadTextElement(int id) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + return await db.TextElements.FirstAsync(n => n.Id == id); + } + + // Adds a new text element: + public static async Task> AddTextElement(string? currentSectionKey, string elementName) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + + if(string.IsNullOrWhiteSpace(currentSectionKey)) + throw new ArgumentOutOfRangeException(nameof(currentSectionKey)); + + var currentSection = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == currentSectionKey); + if (currentSection is null) + throw new ArgumentOutOfRangeException(nameof(currentSectionKey)); + + // Generate a code: + var code = await Utils.GenerateCode(elementName, db.TextElements, (n, code) => n.Section == currentSection && n.Code == code); + + var textElement = new TextElement + { + Name = elementName, + Code = code, + Section = currentSection, + }; + + // Add the new element to the database: + await db.TextElements.AddAsync(textElement); + + try + { + // Save the changes: + await db.SaveChangesAsync(); + return new ProcessorResult(textElement); + } + catch (DbUpdateException updateException) + { + return updateException.ToProcessorResult(); + } + } + + // Renames a text element: + public static async Task> RenameTextElement(int id, string newName) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + + var textElement = await db.TextElements.FirstAsync(n => n.Id == id); + if (textElement is null) + throw new ArgumentOutOfRangeException(nameof(id)); + + // Get the corresponding section: + var section = (await db.TextElements.FirstAsync(n => n.Id == id)).Section; + + // Generate a code: + var code = await Utils.GenerateCode(newName, db.TextElements, (n, code) => n.Section == section && n.Code == code); + + textElement.Name = newName; + textElement.Code = code; + + // Save the changes: + try + { + await db.SaveChangesAsync(); + return new ProcessorResult(textElement); + } + catch (DbUpdateException updateException) + { + return updateException.ToProcessorResult(); + } + } + + // Deletes a text element: + public static async Task DeleteTextElement(int id) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + var textElement = await db.TextElements.FirstAsync(n => n.Id == id); + + // Remove the element from the database: + db.TextElements.Remove(textElement); + + try + { + // Save the changes: + await db.SaveChangesAsync(); + } + catch (DbUpdateException updateException) + { + } + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/Utils.cs b/I18N Commander/Processor/Utils.cs new file mode 100644 index 0000000..9c19df2 --- /dev/null +++ b/I18N Commander/Processor/Utils.cs @@ -0,0 +1,49 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace Processor; + +internal static class Utils +{ + private static readonly Random RNG = new(); + + /// + /// Generates a code out of this name. + /// + /// The name where the code based on + /// The data class + /// The selector to check, if that key already exists. The string parameter is the current code to check. + /// The generated code + internal static async Task GenerateCode(string name, DbSet db, Expression> selector) where TDbSet : class + { + // Filter all non-alphanumeric characters from the name by allowing only A-Z, a-z, 0-9, and spaces from the ASCII table: + name = new string(name.Where(c => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9' or ' ').ToArray()); + + // Remove any whitespaces from the element name, regardless of how many e.g. spaces the user typed: + var code = string.Join('_', name.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToUpperInvariant(); + + // + // The Any() query want's an Expression, but we have an Expression, though. + // Therefore, we have to currying the string away: + var typeDbSet = Expression.Parameter(typeof(TDbSet), null); + var curriedSelector = Expression.Lambda>( + Expression.Invoke(selector, typeDbSet, Expression.Constant(code)), + typeDbSet + ); + + // Check, if this key already exists. If so, add a random number to the end of the key: + if (await db.AnyAsync(curriedSelector)) + while (await db.AnyAsync(curriedSelector)) + { + code += $"_{RNG.Next(1, 10_000)}"; + + // Due to the changed code & since the string is a constant, we have to re-currying the string away: + curriedSelector = Expression.Lambda>( + Expression.Invoke(selector, typeDbSet, Expression.Constant(code)), + typeDbSet + ); + } + + return code; + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/AppEvents.cs b/I18N Commander/UI WinForms/AppEvents.cs new file mode 100644 index 0000000..f4ae16a --- /dev/null +++ b/I18N Commander/UI WinForms/AppEvents.cs @@ -0,0 +1,18 @@ +using DataModel.Database; + +namespace UI_WinForms; + +internal static class AppEvents +{ + // Section changed event which can be subscribed: + internal static event EventHandler
WhenSectionChanged; + + // Method to raise the section changed event: + internal static void SectionChanged(Section section) => AppEvents.WhenSectionChanged?.Invoke(null, section); + + // Text element changed event which can be subscribed: + internal static event EventHandler WhenTextElementChanged; + + // Method to raise the text element changed event: + internal static void TextElementChanged(TextElement textElement) => AppEvents.WhenTextElementChanged?.Invoke(null, textElement); +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/LoaderStart.cs b/I18N Commander/UI WinForms/Components/LoaderStart.cs index 052fe29..9a18582 100644 --- a/I18N Commander/UI WinForms/Components/LoaderStart.cs +++ b/I18N Commander/UI WinForms/Components/LoaderStart.cs @@ -62,6 +62,9 @@ public partial class LoaderStart : UserControl private void buttonOpen_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + if(this.areRecentProjectsVisible) { this.areRecentProjectsVisible = false; @@ -91,6 +94,9 @@ public partial class LoaderStart : UserControl private void buttonNew_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + var saveDialog = new SaveFileDialog { AddExtension = true, diff --git a/I18N Commander/UI WinForms/Components/Main.Designer.cs b/I18N Commander/UI WinForms/Components/Main.Designer.cs index 6e3aef1..7822135 100644 --- a/I18N Commander/UI WinForms/Components/Main.Designer.cs +++ b/I18N Commander/UI WinForms/Components/Main.Designer.cs @@ -30,12 +30,18 @@ { this.tableLayout = new System.Windows.Forms.TableLayoutPanel(); this.flowLayoutBottom = new System.Windows.Forms.FlowLayoutPanel(); - this.splitContainer = new System.Windows.Forms.SplitContainer(); + this.splitContainerLR = new System.Windows.Forms.SplitContainer(); this.sectionTree = new UI_WinForms.Components.SectionTree(); + this.splitContainerRTB = new System.Windows.Forms.SplitContainer(); + this.textElements = new UI_WinForms.Components.TextElements(); this.tableLayout.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); - this.splitContainer.Panel1.SuspendLayout(); - this.splitContainer.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainerLR)).BeginInit(); + this.splitContainerLR.Panel1.SuspendLayout(); + this.splitContainerLR.Panel2.SuspendLayout(); + this.splitContainerLR.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainerRTB)).BeginInit(); + this.splitContainerRTB.Panel1.SuspendLayout(); + this.splitContainerRTB.SuspendLayout(); this.SuspendLayout(); // // tableLayout @@ -44,7 +50,7 @@ this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20F)); this.tableLayout.Controls.Add(this.flowLayoutBottom, 0, 1); - this.tableLayout.Controls.Add(this.splitContainer, 0, 0); + this.tableLayout.Controls.Add(this.splitContainerLR, 0, 0); this.tableLayout.Dock = System.Windows.Forms.DockStyle.Fill; this.tableLayout.Location = new System.Drawing.Point(0, 0); this.tableLayout.Name = "tableLayout"; @@ -63,21 +69,25 @@ this.flowLayoutBottom.Size = new System.Drawing.Size(965, 66); this.flowLayoutBottom.TabIndex = 0; // - // splitContainer + // splitContainerLR // - this.splitContainer.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; - this.splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; - this.splitContainer.Location = new System.Drawing.Point(3, 3); - this.splitContainer.Name = "splitContainer"; + this.splitContainerLR.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.splitContainerLR.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainerLR.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.splitContainerLR.Location = new System.Drawing.Point(3, 3); + this.splitContainerLR.Name = "splitContainerLR"; // - // splitContainer.Panel1 + // splitContainerLR.Panel1 // - this.splitContainer.Panel1.Controls.Add(this.sectionTree); - this.splitContainer.Panel1MinSize = 300; - this.splitContainer.Size = new System.Drawing.Size(959, 531); - this.splitContainer.SplitterDistance = 319; - this.splitContainer.TabIndex = 1; + this.splitContainerLR.Panel1.Controls.Add(this.sectionTree); + this.splitContainerLR.Panel1MinSize = 300; + // + // splitContainerLR.Panel2 + // + this.splitContainerLR.Panel2.Controls.Add(this.splitContainerRTB); + this.splitContainerLR.Size = new System.Drawing.Size(959, 531); + this.splitContainerLR.SplitterDistance = 319; + this.splitContainerLR.TabIndex = 1; // // sectionTree // @@ -88,6 +98,32 @@ this.sectionTree.Size = new System.Drawing.Size(317, 529); this.sectionTree.TabIndex = 0; // + // splitContainerRTB + // + this.splitContainerRTB.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.splitContainerRTB.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainerRTB.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.splitContainerRTB.Location = new System.Drawing.Point(0, 0); + this.splitContainerRTB.Name = "splitContainerRTB"; + this.splitContainerRTB.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainerRTB.Panel1 + // + this.splitContainerRTB.Panel1.Controls.Add(this.textElements); + this.splitContainerRTB.Panel1MinSize = 340; + this.splitContainerRTB.Size = new System.Drawing.Size(636, 531); + this.splitContainerRTB.SplitterDistance = 340; + this.splitContainerRTB.TabIndex = 0; + // + // textElements + // + this.textElements.Dock = System.Windows.Forms.DockStyle.Fill; + this.textElements.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.textElements.Location = new System.Drawing.Point(0, 0); + this.textElements.Name = "textElements"; + this.textElements.Size = new System.Drawing.Size(634, 338); + this.textElements.TabIndex = 0; + // // Main // this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); @@ -97,9 +133,13 @@ this.Name = "Main"; this.Size = new System.Drawing.Size(965, 603); this.tableLayout.ResumeLayout(false); - this.splitContainer.Panel1.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); - this.splitContainer.ResumeLayout(false); + this.splitContainerLR.Panel1.ResumeLayout(false); + this.splitContainerLR.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainerLR)).EndInit(); + this.splitContainerLR.ResumeLayout(false); + this.splitContainerRTB.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainerRTB)).EndInit(); + this.splitContainerRTB.ResumeLayout(false); this.ResumeLayout(false); } @@ -108,7 +148,9 @@ private TableLayoutPanel tableLayout; private FlowLayoutPanel flowLayoutBottom; - private SplitContainer splitContainer; + private SplitContainer splitContainerLR; + private SplitContainer splitContainerRTB; private SectionTree sectionTree; + private TextElements textElements; } } diff --git a/I18N Commander/UI WinForms/Components/SectionTree.cs b/I18N Commander/UI WinForms/Components/SectionTree.cs index a3479b3..308a38c 100644 --- a/I18N Commander/UI WinForms/Components/SectionTree.cs +++ b/I18N Commander/UI WinForms/Components/SectionTree.cs @@ -1,6 +1,4 @@ -using DataModel.Database.Common; -using Microsoft.Extensions.DependencyInjection; -using Processor; +using Processor; using UI_WinForms.Dialogs; using UI_WinForms.Resources; @@ -8,18 +6,14 @@ namespace UI_WinForms.Components; public partial class SectionTree : UserControl { - private readonly DataContext db; - public SectionTree() { this.InitializeComponent(); - - // Get the DI context from the main form: - this.db = Program.SERVICE_PROVIDER.GetService()!; - // Dispose of the context when the control is disposed: - this.Disposed += (_, _) => this.db.Dispose(); - + // Check if we are in the designer: + if(Program.SERVICE_PROVIDER is null) + return; + // Create an image list from a resource: var imgList = new ImageList(); imgList.ImageSize = new Size(45, 45); @@ -35,11 +29,14 @@ public partial class SectionTree : UserControl private async void LoadNodes(object? sender, EventArgs e) { + if(this.DesignMode) + return; + // A dictionary to cache all known tree nodes: var treeNodes = new Dictionary(); // Get the max. depth of the tree: - var maxDepth = await SectionProcessor.GetDepth(this.db); + var maxDepth = await SectionProcessor.GetDepth(); // Store nodes, where we cannot find the parents: var missingParents = new List(); @@ -47,7 +44,7 @@ public partial class SectionTree : UserControl // Populate the tree view out of the database, layer by layer: for (var i = 0; i <= maxDepth; i++) { - await foreach (var section in SectionProcessor.LoadLayer(this.db, i)) + foreach (var section in await SectionProcessor.LoadLayer(i)) { // Create the tree node: var node = new TreeNode @@ -112,6 +109,9 @@ public partial class SectionTree : UserControl private async void buttonAdd_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + var result = InputDialog.Show(new InputDialog.Options( Message: "Please type the desired section name.", Title: "Add a section", @@ -129,13 +129,17 @@ public partial class SectionTree : UserControl var selectedNode = this.treeView.SelectedNode; // Add the new section to the database: - var addedSection = await SectionProcessor.AddSection(this.db, result.Text, addRootNode ? null : selectedNode?.Name); + var addedSection = await SectionProcessor.AddSection(result.Text, addRootNode ? null : selectedNode?.Name); + + addedSection.ProcessError(); + if(!addedSection.Successful) + return; // Add the new section to the tree control: var node = new TreeNode { - Name = addedSection.DataKey, // [sic] name is the key - Text = addedSection.Name, + Name = addedSection.Result!.DataKey, // [sic] name is the key + Text = addedSection.Result.Name, StateImageIndex = 1, }; @@ -151,12 +155,15 @@ public partial class SectionTree : UserControl private async void buttonRemove_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + // Get the currently selected section, which will be removed: var selectedNode = this.treeView.SelectedNode; // Get the number of children: // (notice, that the node's name is its key) - var numberChildren = await SectionProcessor.NumberChildren(this.db, selectedNode.Name); + var numberChildren = await SectionProcessor.NumberChildren(selectedNode.Name); // Ask the user, if he really wants to remove the section: if(MessageBox.Show(numberChildren > 0 ? $"Are you sure, you want to remove the section '{selectedNode.Text}', its {numberChildren} children and so on?" : $"Are you sure, you want to remove the section '{selectedNode.Text}'?", "Remove section", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.No) @@ -164,7 +171,7 @@ public partial class SectionTree : UserControl // Remove the section from the database: // (notice, that the node's name is its key) - await SectionProcessor.RemoveSection(this.db, selectedNode.Name); + await SectionProcessor.RemoveSection(selectedNode.Name); // Remove all nodes from the tree control: this.treeView.Nodes.Clear(); @@ -173,17 +180,31 @@ public partial class SectionTree : UserControl this.LoadNodes(this, EventArgs.Empty); } - private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) + private async void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) { + if(this.DesignMode) + return; + // Get the currently selected section: - var selectedNode = this.treeView.SelectedNode; + var selectedNode = e.Node; // If the selected node is not null, enable the remove & edit button: this.buttonRename.Enabled = this.buttonRemove.Enabled = selectedNode is not null; + + // When a section is selected, fire the event: + if (selectedNode is not null) + { + // Get the section from the database: + var section = await SectionProcessor.GetSection(selectedNode.Name); + AppEvents.SectionChanged(section); + } } private async void buttonRename_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + // Ask the user if he really wants to rename the section: if(MessageBox.Show("Are you sure, you want to rename the selected section? If you are already using this section in your code, you will need to manually refactor your code after renaming it.", "Rename section", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1) == DialogResult.No) return; @@ -204,10 +225,14 @@ public partial class SectionTree : UserControl return; // Rename the section: - var alteredSection = await SectionProcessor.RenameSection(this.db, selectedNode.Name, result.Text); + var alteredSection = await SectionProcessor.RenameSection(selectedNode.Name, result.Text); + + alteredSection.ProcessError(); + if(!alteredSection.Successful) + return; // Rename the selected node: - selectedNode.Text = alteredSection.Name; - selectedNode.Name = alteredSection.DataKey; // [sic] name is the key + selectedNode.Text = alteredSection.Result!.Name; + selectedNode.Name = alteredSection.Result.DataKey; // [sic] name is the key } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/TextElements.Designer.cs b/I18N Commander/UI WinForms/Components/TextElements.Designer.cs new file mode 100644 index 0000000..a9a5cfd --- /dev/null +++ b/I18N Commander/UI WinForms/Components/TextElements.Designer.cs @@ -0,0 +1,216 @@ +namespace UI_WinForms.Components +{ + partial class TextElements + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.tableLayout = new System.Windows.Forms.TableLayoutPanel(); + this.flowLayoutToolbar = new System.Windows.Forms.FlowLayoutPanel(); + this.buttonAdd = new System.Windows.Forms.Button(); + this.buttonRemove = new System.Windows.Forms.Button(); + this.buttonRename = new System.Windows.Forms.Button(); + this.textBoxFilter = new System.Windows.Forms.TextBox(); + this.labelFilter = new System.Windows.Forms.Label(); + this.labelSectionPath = new System.Windows.Forms.Label(); + this.listTextElements = new System.Windows.Forms.ListView(); + this.column = new System.Windows.Forms.ColumnHeader(); + this.toolTip = new System.Windows.Forms.ToolTip(this.components); + this.tableLayout.SuspendLayout(); + this.flowLayoutToolbar.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayout + // + this.tableLayout.ColumnCount = 3; + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F)); + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80F)); + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.Controls.Add(this.flowLayoutToolbar, 0, 0); + this.tableLayout.Controls.Add(this.textBoxFilter, 2, 2); + this.tableLayout.Controls.Add(this.labelFilter, 1, 2); + this.tableLayout.Controls.Add(this.labelSectionPath, 1, 0); + this.tableLayout.Controls.Add(this.listTextElements, 1, 1); + this.tableLayout.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayout.Location = new System.Drawing.Point(0, 0); + this.tableLayout.Name = "tableLayout"; + this.tableLayout.RowCount = 3; + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 40F)); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 40F)); + this.tableLayout.Size = new System.Drawing.Size(706, 201); + this.tableLayout.TabIndex = 0; + // + // flowLayoutToolbar + // + this.flowLayoutToolbar.Controls.Add(this.buttonAdd); + this.flowLayoutToolbar.Controls.Add(this.buttonRemove); + this.flowLayoutToolbar.Controls.Add(this.buttonRename); + this.flowLayoutToolbar.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutToolbar.FlowDirection = System.Windows.Forms.FlowDirection.BottomUp; + this.flowLayoutToolbar.Location = new System.Drawing.Point(0, 0); + this.flowLayoutToolbar.Margin = new System.Windows.Forms.Padding(0); + this.flowLayoutToolbar.Name = "flowLayoutToolbar"; + this.tableLayout.SetRowSpan(this.flowLayoutToolbar, 3); + this.flowLayoutToolbar.Size = new System.Drawing.Size(66, 201); + this.flowLayoutToolbar.TabIndex = 0; + // + // buttonAdd + // + this.buttonAdd.Enabled = false; + this.buttonAdd.FlatAppearance.BorderSize = 0; + this.buttonAdd.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.buttonAdd.Image = global::UI_WinForms.Resources.Icons.icons8_add_tag_512; + this.buttonAdd.Location = new System.Drawing.Point(3, 138); + this.buttonAdd.Name = "buttonAdd"; + this.buttonAdd.Size = new System.Drawing.Size(60, 60); + this.buttonAdd.TabIndex = 0; + this.toolTip.SetToolTip(this.buttonAdd, "Add text element to selected section"); + this.buttonAdd.UseVisualStyleBackColor = true; + this.buttonAdd.Click += new System.EventHandler(this.buttonAdd_Click); + // + // buttonRemove + // + this.buttonRemove.Enabled = false; + this.buttonRemove.FlatAppearance.BorderSize = 0; + this.buttonRemove.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.buttonRemove.Image = global::UI_WinForms.Resources.Icons.icons8_remove_tag_512; + this.buttonRemove.Location = new System.Drawing.Point(3, 72); + this.buttonRemove.Name = "buttonRemove"; + this.buttonRemove.Size = new System.Drawing.Size(60, 60); + this.buttonRemove.TabIndex = 2; + this.toolTip.SetToolTip(this.buttonRemove, "Delete this text element"); + this.buttonRemove.UseVisualStyleBackColor = true; + this.buttonRemove.Click += new System.EventHandler(this.buttonRemove_Click); + // + // buttonRename + // + this.buttonRename.Enabled = false; + this.buttonRename.FlatAppearance.BorderSize = 0; + this.buttonRename.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.buttonRename.Image = global::UI_WinForms.Resources.Icons.icons8_rename_512; + this.buttonRename.Location = new System.Drawing.Point(3, 6); + this.buttonRename.Name = "buttonRename"; + this.buttonRename.Size = new System.Drawing.Size(60, 60); + this.buttonRename.TabIndex = 1; + this.toolTip.SetToolTip(this.buttonRename, "Rename this text element"); + this.buttonRename.UseVisualStyleBackColor = true; + this.buttonRename.Click += new System.EventHandler(this.buttonRename_Click); + // + // textBoxFilter + // + this.textBoxFilter.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxFilter.Location = new System.Drawing.Point(149, 164); + this.textBoxFilter.Name = "textBoxFilter"; + this.textBoxFilter.Size = new System.Drawing.Size(554, 34); + this.textBoxFilter.TabIndex = 2; + this.textBoxFilter.WordWrap = false; + this.textBoxFilter.KeyUp += new System.Windows.Forms.KeyEventHandler(this.textBoxFilter_KeyUp); + // + // labelFilter + // + this.labelFilter.AutoSize = true; + this.labelFilter.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelFilter.Location = new System.Drawing.Point(69, 161); + this.labelFilter.Name = "labelFilter"; + this.labelFilter.Size = new System.Drawing.Size(74, 40); + this.labelFilter.TabIndex = 3; + this.labelFilter.Text = "Filter:"; + this.labelFilter.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // labelSectionPath + // + this.labelSectionPath.AutoSize = true; + this.tableLayout.SetColumnSpan(this.labelSectionPath, 2); + this.labelSectionPath.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelSectionPath.Location = new System.Drawing.Point(69, 0); + this.labelSectionPath.Name = "labelSectionPath"; + this.labelSectionPath.Size = new System.Drawing.Size(634, 40); + this.labelSectionPath.TabIndex = 4; + this.labelSectionPath.Text = "Path"; + this.labelSectionPath.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.toolTip.SetToolTip(this.labelSectionPath, "The path of the currently selected section"); + // + // listTextElements + // + this.listTextElements.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.column}); + this.tableLayout.SetColumnSpan(this.listTextElements, 2); + this.listTextElements.Dock = System.Windows.Forms.DockStyle.Fill; + this.listTextElements.FullRowSelect = true; + this.listTextElements.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; + this.listTextElements.Location = new System.Drawing.Point(69, 43); + this.listTextElements.MultiSelect = false; + this.listTextElements.Name = "listTextElements"; + this.listTextElements.Size = new System.Drawing.Size(634, 115); + this.listTextElements.TabIndex = 5; + this.listTextElements.UseCompatibleStateImageBehavior = false; + this.listTextElements.View = System.Windows.Forms.View.SmallIcon; + this.listTextElements.ItemSelectionChanged += new System.Windows.Forms.ListViewItemSelectionChangedEventHandler(this.listTextElements_ItemSelectionChanged); + // + // column + // + this.column.Width = 194; + // + // toolTip + // + this.toolTip.AutoPopDelay = 30000; + this.toolTip.InitialDelay = 500; + this.toolTip.ReshowDelay = 100; + this.toolTip.ToolTipIcon = System.Windows.Forms.ToolTipIcon.Info; + this.toolTip.ToolTipTitle = "Help"; + // + // TextElements + // + this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.Controls.Add(this.tableLayout); + this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Name = "TextElements"; + this.Size = new System.Drawing.Size(706, 201); + this.tableLayout.ResumeLayout(false); + this.tableLayout.PerformLayout(); + this.flowLayoutToolbar.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private TableLayoutPanel tableLayout; + private FlowLayoutPanel flowLayoutToolbar; + private Button buttonAdd; + private Button buttonRename; + private Button buttonRemove; + private ToolTip toolTip; + private TextBox textBoxFilter; + private Label labelFilter; + private Label labelSectionPath; + private ListView listTextElements; + private ColumnHeader column; + } +} diff --git a/I18N Commander/UI WinForms/Components/TextElements.cs b/I18N Commander/UI WinForms/Components/TextElements.cs new file mode 100644 index 0000000..abed227 --- /dev/null +++ b/I18N Commander/UI WinForms/Components/TextElements.cs @@ -0,0 +1,164 @@ +using DataModel.Database; +using Processor; +using UI_WinForms.Dialogs; +using UI_WinForms.Resources; + +namespace UI_WinForms.Components; + +public partial class TextElements : UserControl +{ + private Section? currentSection; + private TextElement? currentTextElement; + + public TextElements() + { + this.InitializeComponent(); + + // Check if we are in the designer: + if(Program.SERVICE_PROVIDER is null) + return; + + // Create an image list from a resource: + var imgList = new ImageList(); + imgList.ImageSize = new Size(45, 45); + imgList.ColorDepth = ColorDepth.Depth32Bit; + imgList.Images.Add(Icons.icons8_align_text_left_512); + + // Set the image list to the list box: + this.listTextElements.SmallImageList = imgList; + + // When the section is changed, update this component: + AppEvents.WhenSectionChanged += async (sender, section) => + { + this.currentSection = section; + this.currentTextElement = null; + this.buttonAdd.Enabled = this.currentSection is not null; + this.buttonRename.Enabled = this.buttonRemove.Enabled = false; + + if (this.currentSection is null) + return; + + // Update the path: + this.labelSectionPath.Text = await SectionProcessor.GetSectionPath(this.currentSection.DataKey); + await this.LoadTextElements(); + }; + + // When the text element is changed, update the button states: + AppEvents.WhenTextElementChanged += (sender, textElement) => + { + this.currentTextElement = textElement; + this.buttonRename.Enabled = this.buttonRemove.Enabled = this.currentTextElement is not null; + }; + } + + // Loads all the text elements for the current section. + private async Task LoadTextElements() + { + if (this.currentSection is null) + return; + + // Load the text elements: + var textElements = await TextElementProcessor.GetTextElements(this.currentSection, this.textBoxFilter.Text); + + // Update the list: + this.listTextElements.Items.Clear(); + foreach (var textElement in textElements) + this.listTextElements.Items.Add(new ListViewItem(textElement.Name, 0) + { + Tag = textElement.Id, + }); + + this.column.AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent); + } + + private async void buttonAdd_Click(object sender, EventArgs e) + { + if(this.DesignMode) + return; + + var result = InputDialog.Show(new InputDialog.Options( + Message: "Please type the desired text element's name.", + Title: "Add a text element", + Placeholder: "My text element", + ShowQuestionCheckbox: false + )); + + if(result.DialogResult == DialogResult.Cancel) + return; + + // Add the text element to the database into the current section: + var newTextElement = await TextElementProcessor.AddTextElement(this.currentSection?.DataKey, result.Text); + newTextElement.ProcessError(); + + if(!newTextElement.Successful) + return; + + // Add the text element to the list: + this.listTextElements.Items.Add(new ListViewItem(newTextElement.Result!.Name, 0) + { + Tag = newTextElement.Result.Id, + }); + + this.column.AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent); + } + + private async void buttonRemove_Click(object sender, EventArgs e) + { + if(this.DesignMode || this.currentTextElement is null) + return; + + // Ask the user, if he really wants to remove the text element: + if(MessageBox.Show(this.currentTextElement.Translations.Count > 0 ? $"Are you sure, you want to remove the text element '{this.currentTextElement.Name}', its {this.currentTextElement.Translations.Count} translations and so on?" : $"Are you sure, you want to remove the text element '{this.currentTextElement.Name}'?", "Remove text element", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.No) + return; + + // Remove the text element1 from the database: + await TextElementProcessor.DeleteTextElement(this.currentTextElement.Id); + + // Reload the data: + await this.LoadTextElements(); + } + + private async void buttonRename_Click(object sender, EventArgs e) + { + if(this.DesignMode) + return; + + // Ask the user if he really wants to rename the text element: + if(MessageBox.Show("Are you sure, you want to rename the selected text element? If you are already using this element in your code, you will need to manually refactor your code after renaming it.", "Rename text element", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1) == DialogResult.No) + return; + + // Ask the user for the new name: + var result = InputDialog.Show(new InputDialog.Options( + Message: "Please edit the text element's name.", + PreloadedText: this.currentTextElement!.Name, + ShowQuestionCheckbox: false, + Title: "Rename text element" + )); + + // If the user canceled, return: + if(result.DialogResult == DialogResult.Cancel) + return; + + // Rename the text element: + var alteredTextElement = await TextElementProcessor.RenameTextElement(this.currentTextElement.Id, result.Text); + + alteredTextElement.ProcessError(); + if(!alteredTextElement.Successful) + return; + + // Reload the text elements: + await this.LoadTextElements(); + } + + private async void listTextElements_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) + { + // Load the text element: + var selectedTextElementId = (int)e.Item.Tag; + this.currentTextElement = await TextElementProcessor.LoadTextElement(selectedTextElementId); + + // Fire the event: + AppEvents.TextElementChanged(this.currentTextElement); + } + + private async void textBoxFilter_KeyUp(object sender, KeyEventArgs e) => await this.LoadTextElements(); +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/TextElements.resx b/I18N Commander/UI WinForms/Components/TextElements.resx new file mode 100644 index 0000000..99de901 --- /dev/null +++ b/I18N Commander/UI WinForms/Components/TextElements.resx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Dialogs/InputDialog.cs b/I18N Commander/UI WinForms/Dialogs/InputDialog.cs index 25afee0..eb92d63 100644 --- a/I18N Commander/UI WinForms/Dialogs/InputDialog.cs +++ b/I18N Commander/UI WinForms/Dialogs/InputDialog.cs @@ -48,6 +48,9 @@ public partial class InputDialog : Form private void buttonOk_Click(object sender, EventArgs e) { + if(this.DesignMode) + return; + if (!string.IsNullOrWhiteSpace(this.textBoxInput.Text)) { this.DialogResult = DialogResult.OK; diff --git a/I18N Commander/UI WinForms/ExtensionsError.cs b/I18N Commander/UI WinForms/ExtensionsError.cs new file mode 100644 index 0000000..e21b8ee --- /dev/null +++ b/I18N Commander/UI WinForms/ExtensionsError.cs @@ -0,0 +1,13 @@ +using Processor; + +namespace UI_WinForms; + +public static class ExtensionsError +{ + public static void ProcessError(this ProcessorResult result) + { + if (result.Successful) return; + + MessageBox.Show(result.ErrorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Loader.cs b/I18N Commander/UI WinForms/Loader.cs index 38065c7..6a6ca87 100644 --- a/I18N Commander/UI WinForms/Loader.cs +++ b/I18N Commander/UI WinForms/Loader.cs @@ -11,6 +11,9 @@ public partial class Loader : Form private void loaderStart_LoadProject(object sender, string projectFilePath) { + if(this.DesignMode) + return; + this.DataFile = projectFilePath; this.DialogResult = DialogResult.OK; this.Close(); diff --git a/I18N Commander/UI WinForms/Main.Designer.cs b/I18N Commander/UI WinForms/Main.Designer.cs index 3189ccc..edf901e 100644 --- a/I18N Commander/UI WinForms/Main.Designer.cs +++ b/I18N Commander/UI WinForms/Main.Designer.cs @@ -3,12 +3,12 @@ partial class Main { /// - /// Required designer variable. + /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// - /// Clean up any resources being used. + /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) @@ -23,8 +23,8 @@ #region Windows Form Designer generated code /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. /// private void InitializeComponent() { @@ -38,14 +38,14 @@ this.mainComponent.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); this.mainComponent.Location = new System.Drawing.Point(0, 0); this.mainComponent.Name = "mainComponent"; - this.mainComponent.Size = new System.Drawing.Size(1071, 755); + this.mainComponent.Size = new System.Drawing.Size(1902, 1033); this.mainComponent.TabIndex = 0; // // Main // this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(1071, 755); + this.ClientSize = new System.Drawing.Size(1902, 1033); this.Controls.Add(this.mainComponent); this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 5c21401..e871b6e 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -2,13 +2,14 @@ using DataModel; using DataModel.Database.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Processor; namespace UI_WinForms; internal static class Program { internal const string VERSION = "v0.1.0"; - internal static IServiceProvider SERVICE_PROVIDER = null!; + internal static IServiceProvider? SERVICE_PROVIDER; [STAThread] private static void Main() @@ -34,9 +35,6 @@ internal static class Program // builder.ConfigureServices((hostContext, serviceCollection) => { - // The main form: - serviceCollection.AddSingleton
(); - // The database: serviceCollection.AddDatabase(loader.DataFile, true); }); @@ -50,15 +48,15 @@ internal static class Program // Get a service provider: SERVICE_PROVIDER = scope.ServiceProvider; + // Set the service provider to the processor: + ProcessorMeta.ServiceProvider = SERVICE_PROVIDER; + // Apply database migrations: using (var database = SERVICE_PROVIDER.GetRequiredService()) Setup.PerformDataMigration(database).Wait(); - // Create the main window: - var mainWindow = SERVICE_PROVIDER.GetService
(); - // Start the app: - Application.Run(mainWindow); + Application.Run(new Main()); } } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs index a97cc64..8311735 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs +++ b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs @@ -70,6 +70,26 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_add_tag_512 { + get { + object obj = ResourceManager.GetObject("icons8_add_tag_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_align_text_left_512 { + get { + object obj = ResourceManager.GetObject("icons8_align_text_left_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -160,6 +180,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_remove_tag_512 { + get { + object obj = ResourceManager.GetObject("icons8_remove_tag_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/I18N Commander/UI WinForms/Resources/Icons.resx b/I18N Commander/UI WinForms/Resources/Icons.resx index 0894a59..41a3205 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.resx +++ b/I18N Commander/UI WinForms/Resources/Icons.resx @@ -121,6 +121,12 @@ icons8-add-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-add-tag-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + icons8-align-text-left-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-browse-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -148,6 +154,9 @@ icons8-open-file-under-cursor-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-remove-tag-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-rename-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/I18N Commander/UI WinForms/Resources/icons8-add-tag-512.png b/I18N Commander/UI WinForms/Resources/icons8-add-tag-512.png new file mode 100644 index 0000000..1737c75 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-add-tag-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-align-text-left-512.png b/I18N Commander/UI WinForms/Resources/icons8-align-text-left-512.png new file mode 100644 index 0000000..4e67e1d Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-align-text-left-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-remove-tag-512.png b/I18N Commander/UI WinForms/Resources/icons8-remove-tag-512.png new file mode 100644 index 0000000..0bd6ee6 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-remove-tag-512.png differ diff --git a/I18N Commander/UI WinForms/UI WinForms.csproj b/I18N Commander/UI WinForms/UI WinForms.csproj index dc5c59a..390c5ab 100644 --- a/I18N Commander/UI WinForms/UI WinForms.csproj +++ b/I18N Commander/UI WinForms/UI WinForms.csproj @@ -8,6 +8,7 @@ true enable default + I18N Commander