Merge branch '11-component-text-element-navigator' into 'main'

Resolve "Component: Text Element Navigator"

Closes #11, #29, #27, and #26

See merge request open-source/dotnet/i18n-commander!9
This commit is contained in:
Thorsten 2022-07-23 19:29:16 +00:00
commit 8706d045dd
31 changed files with 1353 additions and 93 deletions

View File

@ -37,6 +37,8 @@ public sealed class DataContext : DbContext
modelBuilder.Entity<Section>().HasIndex(n => n.Name);
modelBuilder.Entity<Section>().HasIndex(n => n.Depth);
modelBuilder.Entity<Section>().HasIndex(n => n.DataKey);
// modelBuilder.Entity<Section>().Navigation(n => n.Parent).AutoInclude(); // Cycle-reference, does not work, though.
modelBuilder.Entity<Section>().Navigation(n => n.TextElements).AutoInclude();
#endregion
@ -44,6 +46,8 @@ public sealed class DataContext : DbContext
modelBuilder.Entity<TextElement>().HasIndex(n => n.Id);
modelBuilder.Entity<TextElement>().HasIndex(n => n.Code);
modelBuilder.Entity<TextElement>().HasIndex(n => n.Name);
modelBuilder.Entity<TextElement>().Navigation(n => n.Section).AutoInclude();
#endregion
@ -52,6 +56,7 @@ public sealed class DataContext : DbContext
modelBuilder.Entity<Translation>().HasIndex(n => n.Id);
modelBuilder.Entity<Translation>().HasIndex(n => n.Culture);
modelBuilder.Entity<Translation>().HasIndex(n => n.Text);
modelBuilder.Entity<Translation>().Navigation(n => n.TextElement).AutoInclude();
#endregion
}

View File

@ -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<Translation> Translations { get; set; }
public List<Translation> Translations { get; set; } = new();
}

View File

@ -0,0 +1,201 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Depth")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("BoolValue")
.HasColumnType("INTEGER");
b.Property<Guid>("GuidValue")
.HasColumnType("TEXT");
b.Property<int>("IntegerValue")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Culture")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("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
}
}
}

View File

@ -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<string>(
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");
}
}
}

View File

@ -103,6 +103,10 @@ namespace DataModel.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SectionId")
.HasColumnType("INTEGER");
@ -112,6 +116,8 @@ namespace DataModel.Migrations
b.HasIndex("Id");
b.HasIndex("Name");
b.HasIndex("SectionId");
b.ToTable("TextElements");

View File

@ -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;
/// <summary>
/// Tries to migrate the data file.
/// </summary>
@ -30,7 +34,8 @@ public static class Setup
/// </summary>
public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true)
{
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)}"), ServiceLifetime.Transient);
Setup.usedDataFile = path2DataFile;
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient);
}
/// <summary>

View File

@ -0,0 +1,34 @@
using System.Text;
using DataModel;
using Microsoft.Data.Sqlite;
namespace Processor;
public static class ExtensionsError
{
public static ProcessorResult<TResult> ToProcessorResult<TResult>(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<TResult>(null, false, sb.ToString());
}
else
return new ProcessorResult<TResult>(null, false, $"A database error occurred: '{sqliteException.Message}'");
return new ProcessorResult<TResult>(null, false, $"A database error occurred: '{exception.Message}'");
}
}

View File

@ -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);
/// <summary>
/// Find out what process(es) have a lock on the specified file.
/// </summary>
/// <param name="path">Path of the file.</param>
/// <returns>Processes locking the file</returns>
/// <remarks>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)
/// </remarks>
public static List<Process> WhoIsLocking(string path)
{
var key = Guid.NewGuid().ToString();
var processes = new List<Process>();
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;
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,3 @@
namespace Processor;
public readonly record struct ProcessorResult<TResult>(TResult? Result, bool Successful = true, string ErrorMessage = "");

View File

@ -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
/// <summary>
/// Load one layer of the tree by using the specified depth:
/// </summary>
public static IAsyncEnumerable<Section> LoadLayer(DataContext db, int depth)
public static async Task<List<Section>> LoadLayer(int depth)
{
return db.Sections.Where(n => n.Depth == depth).OrderBy(n => n.Id).AsAsyncEnumerable();
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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;
}
/// <summary>
/// Determine how deep the tree is.
/// </summary>
public static async ValueTask<int> GetDepth(DataContext db)
public static async ValueTask<int> GetDepth()
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
if(!await db.Sections.AnyAsync())
{
return 0;
@ -30,24 +39,15 @@ public static class SectionProcessor
/// <summary>
/// Compute the new sections key and its depth, then store the section in the database.
/// </summary>
public static async Task<Section> AddSection(DataContext db, string text, string? parentKey)
public static async Task<ProcessorResult<Section>> 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<DataContext>();
// 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<Section>(rootSection);
}
catch (Exception e)
{
return e.ToProcessorResult<Section>();
}
}
// 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>(section);
}
catch (Exception e)
{
return e.ToProcessorResult<Section>();
}
}
public static async Task RemoveSection(DataContext db, string selectedKey)
public static async Task RemoveSection(string selectedKey)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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<int> NumberChildren(DataContext db, string selectedKey)
public static async Task<int> NumberChildren(string selectedKey)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// 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<Section> RenameSection(DataContext db, string selectedNodeKey, string alteredName)
public static async Task<ProcessorResult<Section>> RenameSection(string selectedNodeKey, string alteredName)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// 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>(section);
}
catch (Exception e)
{
return e.ToProcessorResult<Section>();
}
}
public static async Task<Section> GetSection(string sectionKey)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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<string> GetSectionPath(string sectionKey)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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}";
}
}

View File

@ -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<List<TextElement>> GetTextElements(Section section, string filterTerm)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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<TextElement> LoadTextElement(int id)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
return await db.TextElements.FirstAsync(n => n.Id == id);
}
// Adds a new text element:
public static async Task<ProcessorResult<TextElement>> AddTextElement(string? currentSectionKey, string elementName)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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>(textElement);
}
catch (DbUpdateException updateException)
{
return updateException.ToProcessorResult<TextElement>();
}
}
// Renames a text element:
public static async Task<ProcessorResult<TextElement>> RenameTextElement(int id, string newName)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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>(textElement);
}
catch (DbUpdateException updateException)
{
return updateException.ToProcessorResult<TextElement>();
}
}
// Deletes a text element:
public static async Task DeleteTextElement(int id)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
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)
{
}
}
}

View File

@ -0,0 +1,49 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace Processor;
internal static class Utils
{
private static readonly Random RNG = new();
/// <summary>
/// Generates a code out of this name.
/// </summary>
/// <param name="name">The name where the code based on</param>
/// <param name="db">The data class</param>
/// <param name="selector">The selector to check, if that key already exists. The string parameter is the current code to check.</param>
/// <returns>The generated code</returns>
internal static async Task<string> GenerateCode<TDbSet>(string name, DbSet<TDbSet> db, Expression<Func<TDbSet, string, bool>> 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<T, bool>, but we have an Expression<T, string, bool>, though.
// Therefore, we have to currying the string away:
var typeDbSet = Expression.Parameter(typeof(TDbSet), null);
var curriedSelector = Expression.Lambda<Func<TDbSet, bool>>(
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<Func<TDbSet, bool>>(
Expression.Invoke(selector, typeDbSet, Expression.Constant(code)),
typeDbSet
);
}
return code;
}
}

View File

@ -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<Section> 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<TextElement> WhenTextElementChanged;
// Method to raise the text element changed event:
internal static void TextElementChanged(TextElement textElement) => AppEvents.WhenTextElementChanged?.Invoke(null, textElement);
}

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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<DataContext>()!;
// 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<string, TreeNode>();
// 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<TreeNode>();
@ -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
}
}

View File

@ -0,0 +1,216 @@
namespace UI_WinForms.Components
{
partial class TextElements
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@ -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();
}

View File

@ -0,0 +1,63 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@ -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;

View File

@ -0,0 +1,13 @@
using Processor;
namespace UI_WinForms;
public static class ExtensionsError
{
public static void ProcessError<TResult>(this ProcessorResult<TResult> result)
{
if (result.Successful) return;
MessageBox.Show(result.ErrorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

View File

@ -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();

View File

@ -3,12 +3,12 @@
partial class Main
{
/// <summary>
/// Required designer variable.
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
@ -23,8 +23,8 @@
#region Windows Form Designer generated code
/// <summary>
/// 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.
/// </summary>
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")));

View File

@ -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<Main>();
// 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<DataContext>())
Setup.PerformDataMigration(database).Wait();
// Create the main window:
var mainWindow = SERVICE_PROVIDER.GetService<Main>();
// Start the app:
Application.Run(mainWindow);
Application.Run(new Main());
}
}
}

View File

@ -70,6 +70,26 @@ namespace UI_WinForms.Resources {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap icons8_add_tag_512 {
get {
object obj = ResourceManager.GetObject("icons8_add_tag_512", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
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));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
@ -160,6 +180,16 @@ namespace UI_WinForms.Resources {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap icons8_remove_tag_512 {
get {
object obj = ResourceManager.GetObject("icons8_remove_tag_512", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@ -121,6 +121,12 @@
<data name="icons8_add_folder_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-add-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_add_tag_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-add-tag-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_align_text_left_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-align-text-left-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_browse_folder_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-browse-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
@ -148,6 +154,9 @@
<data name="icons8_open_file_under_cursor_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-open-file-under-cursor-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_remove_tag_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-remove-tag-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_rename_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-rename-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -8,6 +8,7 @@
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>default</LangVersion>
<AssemblyName>I18N Commander</AssemblyName>
</PropertyGroup>
<ItemGroup>