Skip to content

Commit 2550a67

Browse files
authored
feat: add table/field schema editor with DataGrid (#146)
## Summary - Add FmTable/FmField model with full XML round-trip for schema editing - Add DataGrid-based table editor with add/remove fields, type/kind selection, and calculation editor modal - Add typed New menu with Script (Ctrl+N) and Table (Ctrl+Shift+N) creation - Fix missing DataGrid theme StyleInclude and command CanExecute refresh on selection change
1 parent 4e8b985 commit 2550a67

60 files changed

Lines changed: 4345 additions & 211 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,77 @@ Note: SharpFM and FileMaker must be running on the same computer. In order to sh
88

99
- Head over to [Releases](https://github.com/fuzzzerd/SharpFM/releases), grab the latest version (binaries for Windows, Mac, Linux are all available there).
1010

11-
### Clipping from FileMaker
11+
### Importing Clips from FileMaker
1212

1313
- Open SharpFM.
1414
- Switch over to FileMaker.
15-
- Copy something to the clipboard.
15+
- Copy something to the clipboard (scripts, tables, layouts, etc).
1616
- Switch back to SharpFM.
17-
- Use the Edit menu to "Paste from FileMaker Blob".
18-
- See your object(s) in the clips list with the Xml editor on the side.
17+
- Use **File > New > From Clipboard** (`Ctrl+V`) to import the clip.
18+
- The clip appears in the left panel with the appropriate editor on the right.
1919

20-
### Clipping from SharpFM to FileMaker
20+
### Exporting Clips to FileMaker
2121

22-
- Ensure you have a clip in SharpFM
23-
- Select the clip in the list
24-
- Use the Edit menu to "Copy As FileMaker Blob"
25-
- Switch to FileMaker: based on the clip type, open Database manger, Script manager, layout mode, etc.
26-
- Paste into FileMaker as you normally would.
22+
- Select a clip in the left panel.
23+
- Use **File > Save > Selected clip to Clipboard** (`Ctrl+Shift+C`).
24+
- Switch to FileMaker and open the appropriate destination (Database Manager, Script Workspace, Layout mode, etc).
25+
- Paste as you normally would.
2726

28-
### Saving / Sharing XML Clips
27+
### Editing Scripts
2928

30-
This is an area we can improve, with interoperability with some other similar tools. More to come? Contributions welcome.
29+
- Select a script clip or create one with **File > New > Script** (`Ctrl+N`).
30+
- The script editor shows a plain-text representation of the script steps with FmScript syntax highlighting.
31+
- Edit the script text directly; changes are synced back to the underlying XML.
3132

32-
SharpFM has the option to persist clips between sessions by using the File menu to "Save to Db".
33+
### Editing Tables
3334

34-
- Save the XML for a given clip as a separate file (copy/paste to Notepad, Nano, email body, etc)
35-
- Share the resulting XML file.
36-
- Use the File menu to create a New clip.
37-
- Select the appropriate clip type (Table, Script, Layout, etc)
38-
- Paste the raw XML into the code editor.
35+
- Select a table clip or create one with **File > New > Table** (`Ctrl+Shift+N`).
36+
- The table editor shows a DataGrid with columns for Field Name, Type, Kind, Required, Unique, and Comment.
37+
- Click **+ Add Field** to add a new field, then edit its properties inline.
38+
- Select a field and click **Remove** or press `Delete` to remove it.
39+
- Change a field's Kind to Calculated or Summary, then click **Edit Calculation...** to open the calculation editor.
40+
41+
### Viewing Raw XML
42+
43+
- Select any clip and use **View > Show XML** (`Ctrl+Shift+X`) to open the raw XML in a separate window.
44+
- Edits made in the XML window are synced back to the clip when the window is closed.
45+
46+
### Saving and Sharing Clips
47+
48+
SharpFM persists clips between sessions as XML files in a local folder.
49+
50+
- Use **File > Save > Save All To Folder** (`Ctrl+S`) to save all clips.
51+
- Use **File > Open Folder** to load clips from a different folder.
52+
- The clip files are plain XML and can be shared via git, email, or any text-based tool.
3953

4054
## Features
4155

4256
- [x] Copy FileMaker Scripts, Tables, or Layouts From FileMaker Pro to their XML representation and back into FileMaker.
4357
- [x] Store FileMaker Scripts, Tables, and Layouts to xml files that can be shared via git, email or other text based tools.
4458
- [x] Edit raw FileMaker XML code (scripts, layouts, tables) with ability to paste changes back into FileMaker.
4559
- [x] Use AvaloniaEdit for XML editing with XML syntax highlighting.
46-
- [ ] Better UI tools to mutate the Raw XML.
60+
- [x] Plain-text script editor with FmScript syntax highlighting.
61+
- [x] DataGrid table/field editor with inline editing, calculation editor, and type/kind selection.
62+
- [x] View and edit raw XML alongside structured editors.
63+
64+
## Plugins
65+
66+
SharpFM supports plugins via the `SharpFM.Plugin` contract library. Plugins implement `IPanelPlugin` and are loaded from the `plugins/` directory at startup. You can also install and manage plugins from the **View > Manage Plugins...** menu.
67+
68+
A sample "Clip Inspector" plugin is included to demonstrate the plugin API.
69+
70+
### Writing a Plugin
71+
72+
1. Create a new .NET 8 class library referencing `SharpFM.Plugin`.
73+
2. Implement `IPanelPlugin` — provide an `Id`, `DisplayName`, and `CreatePanel()` returning an Avalonia `Control`.
74+
3. Use `IPluginHost` in `Initialize()` to observe clip selection and push XML updates.
75+
4. Build your DLL and drop it in the `plugins/` directory.
76+
77+
See `src/SharpFM.Plugin.Sample/` for a complete working example.
78+
79+
### License
80+
81+
While SharpFM is licensed under GPL v3, plugins that communicate solely through the interfaces in `SharpFM.Plugin` are not required to be GPL-licensed. See the plugin interface source files for the full exception clause.
4782

4883
## Troubleshooting
4984

SharpFM.sln

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E2FF2BB3
99
EndProject
1010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Tests", "tests\SharpFM.Tests\SharpFM.Tests.csproj", "{5B228160-ECB9-4DFC-91D7-413AE9900617}"
1111
EndProject
12+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1515B0F2-1419-4778-92A8-430A8B4931F7}"
13+
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin", "src\SharpFM.Plugin\SharpFM.Plugin.csproj", "{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin.Sample", "src\SharpFM.Plugin.Sample\SharpFM.Plugin.Sample.csproj", "{0ACF3F64-A87C-487C-B780-B39327C1B801}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin.Tests", "tests\SharpFM.Plugin.Tests\SharpFM.Plugin.Tests.csproj", "{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB}"
19+
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Plugin.XmlViewer", "src\SharpFM.Plugin.XmlViewer\SharpFM.Plugin.XmlViewer.csproj", "{E988ECF3-E096-4F29-88C0-27B50FD6C703}"
21+
EndProject
1222
Global
1323
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1424
Debug|Any CPU = Debug|Any CPU
@@ -30,8 +40,28 @@ Global
3040
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Debug|Any CPU.Build.0 = Debug|Any CPU
3141
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Release|Any CPU.ActiveCfg = Release|Any CPU
3242
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Release|Any CPU.Build.0 = Release|Any CPU
43+
{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44+
{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395}.Debug|Any CPU.Build.0 = Debug|Any CPU
45+
{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{0ACF3F64-A87C-487C-B780-B39327C1B801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48+
{0ACF3F64-A87C-487C-B780-B39327C1B801}.Debug|Any CPU.Build.0 = Debug|Any CPU
49+
{0ACF3F64-A87C-487C-B780-B39327C1B801}.Release|Any CPU.ActiveCfg = Release|Any CPU
50+
{0ACF3F64-A87C-487C-B780-B39327C1B801}.Release|Any CPU.Build.0 = Release|Any CPU
51+
{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52+
{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB}.Debug|Any CPU.Build.0 = Debug|Any CPU
53+
{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB}.Release|Any CPU.ActiveCfg = Release|Any CPU
54+
{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB}.Release|Any CPU.Build.0 = Release|Any CPU
55+
{E988ECF3-E096-4F29-88C0-27B50FD6C703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56+
{E988ECF3-E096-4F29-88C0-27B50FD6C703}.Debug|Any CPU.Build.0 = Debug|Any CPU
57+
{E988ECF3-E096-4F29-88C0-27B50FD6C703}.Release|Any CPU.ActiveCfg = Release|Any CPU
58+
{E988ECF3-E096-4F29-88C0-27B50FD6C703}.Release|Any CPU.Build.0 = Release|Any CPU
3359
EndGlobalSection
3460
GlobalSection(NestedProjects) = preSolution
3561
{5B228160-ECB9-4DFC-91D7-413AE9900617} = {E2FF2BB3-AF37-44BA-BD84-999B352D814E}
62+
{2D7BC534-E63F-4FC2-84F1-62BC0E8A1395} = {1515B0F2-1419-4778-92A8-430A8B4931F7}
63+
{0ACF3F64-A87C-487C-B780-B39327C1B801} = {1515B0F2-1419-4778-92A8-430A8B4931F7}
64+
{74337D8E-5EC6-4E5F-9E9E-F2B59E8ECABB} = {E2FF2BB3-AF37-44BA-BD84-999B352D814E}
65+
{E988ECF3-E096-4F29-88C0-27B50FD6C703} = {1515B0F2-1419-4778-92A8-430A8B4931F7}
3666
EndGlobalSection
3767
EndGlobal
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<UserControl
2+
xmlns="https://github.com/avaloniaui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:local="using:SharpFM.Plugin.Sample"
5+
x:Class="SharpFM.Plugin.Sample.ClipInspectorPanel"
6+
x:DataType="local:ClipInspectorViewModel">
7+
8+
<StackPanel Margin="16" Spacing="12">
9+
<TextBlock Classes="Fluent2Subtitle" Text="Clip Inspector" />
10+
11+
<StackPanel Spacing="8" IsVisible="{Binding HasClip}">
12+
<StackPanel Spacing="2">
13+
<TextBlock Classes="Fluent2Caption" Opacity="0.7" Text="Name" />
14+
<TextBlock Classes="Fluent2Body" Text="{Binding ClipName}" />
15+
</StackPanel>
16+
17+
<StackPanel Spacing="2">
18+
<TextBlock Classes="Fluent2Caption" Opacity="0.7" Text="Type" />
19+
<TextBlock Classes="Fluent2Body" Text="{Binding ClipType}" />
20+
</StackPanel>
21+
22+
<StackPanel Spacing="2">
23+
<TextBlock Classes="Fluent2Caption" Opacity="0.7" Text="XML Elements" />
24+
<TextBlock Classes="Fluent2Body" Text="{Binding ElementCount}" />
25+
</StackPanel>
26+
27+
<StackPanel Spacing="2">
28+
<TextBlock Classes="Fluent2Caption" Opacity="0.7" Text="Approx. Size" />
29+
<TextBlock Classes="Fluent2Body" Text="{Binding XmlSize}" />
30+
</StackPanel>
31+
</StackPanel>
32+
33+
<TextBlock
34+
Classes="Fluent2Body"
35+
Opacity="0.5"
36+
IsVisible="{Binding !HasClip}"
37+
Text="Select a clip to inspect its metadata." />
38+
</StackPanel>
39+
40+
</UserControl>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Markup.Xaml;
3+
4+
namespace SharpFM.Plugin.Sample;
5+
6+
public partial class ClipInspectorPanel : UserControl
7+
{
8+
public ClipInspectorPanel()
9+
{
10+
InitializeComponent();
11+
}
12+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Avalonia.Controls;
4+
using SharpFM.Plugin;
5+
6+
namespace SharpFM.Plugin.Sample;
7+
8+
public class ClipInspectorPlugin : IPanelPlugin
9+
{
10+
public string Id => "clip-inspector";
11+
public string DisplayName => "Clip Inspector";
12+
public IReadOnlyList<PluginKeyBinding> KeyBindings => [];
13+
public IReadOnlyList<PluginMenuAction> MenuActions => [];
14+
15+
private IPluginHost? _host;
16+
private ClipInspectorViewModel? _viewModel;
17+
18+
public void Initialize(IPluginHost host)
19+
{
20+
_host = host;
21+
_host.SelectedClipChanged += OnSelectedClipChanged;
22+
_host.ClipContentChanged += OnClipContentChanged;
23+
}
24+
25+
public Control CreatePanel()
26+
{
27+
_viewModel = new ClipInspectorViewModel();
28+
_viewModel.Update(_host?.SelectedClip);
29+
return new ClipInspectorPanel { DataContext = _viewModel };
30+
}
31+
32+
private void OnSelectedClipChanged(object? sender, ClipInfo? clip)
33+
{
34+
_viewModel?.Update(clip);
35+
}
36+
37+
private void OnClipContentChanged(object? sender, ClipContentChangedArgs args)
38+
{
39+
_viewModel?.Update(args.Clip);
40+
}
41+
42+
public void Dispose()
43+
{
44+
if (_host is null) return;
45+
_host.SelectedClipChanged -= OnSelectedClipChanged;
46+
_host.ClipContentChanged -= OnClipContentChanged;
47+
}
48+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Xml.Linq;
6+
using SharpFM.Plugin;
7+
8+
namespace SharpFM.Plugin.Sample;
9+
10+
public class ClipInspectorViewModel : INotifyPropertyChanged
11+
{
12+
public event PropertyChangedEventHandler? PropertyChanged;
13+
14+
private void Notify([CallerMemberName] string name = "")
15+
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
16+
17+
private string _clipName = "(no clip selected)";
18+
public string ClipName { get => _clipName; private set { _clipName = value; Notify(); } }
19+
20+
private string _clipType = "-";
21+
public string ClipType { get => _clipType; private set { _clipType = value; Notify(); } }
22+
23+
private string _elementCount = "-";
24+
public string ElementCount { get => _elementCount; private set { _elementCount = value; Notify(); } }
25+
26+
private string _xmlSize = "-";
27+
public string XmlSize { get => _xmlSize; private set { _xmlSize = value; Notify(); } }
28+
29+
private bool _hasClip;
30+
public bool HasClip { get => _hasClip; private set { _hasClip = value; Notify(); } }
31+
32+
public void Update(ClipInfo? clip)
33+
{
34+
if (clip is null)
35+
{
36+
ClipName = "(no clip selected)";
37+
ClipType = "-";
38+
ElementCount = "-";
39+
XmlSize = "-";
40+
HasClip = false;
41+
return;
42+
}
43+
44+
HasClip = true;
45+
ClipName = clip.Name;
46+
ClipType = clip.ClipType;
47+
XmlSize = FormatBytes(clip.Xml.Length * 2); // rough UTF-16 estimate
48+
49+
try
50+
{
51+
var doc = XDocument.Parse(clip.Xml);
52+
var count = doc.Descendants().Count();
53+
ElementCount = count.ToString();
54+
}
55+
catch
56+
{
57+
ElementCount = "(invalid XML)";
58+
}
59+
}
60+
61+
private static string FormatBytes(int bytes) => bytes switch
62+
{
63+
< 1024 => $"{bytes} B",
64+
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
65+
_ => $"{bytes / (1024.0 * 1024.0):F1} MB"
66+
};
67+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<LangVersion>latest</LangVersion>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\SharpFM.Plugin\SharpFM.Plugin.csproj" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Avalonia" Version="11.2.4" />
14+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
15+
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<LangVersion>latest</LangVersion>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\SharpFM.Plugin\SharpFM.Plugin.csproj" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Avalonia" Version="11.2.4" />
14+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
15+
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
16+
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.66" />
17+
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
18+
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
19+
</ItemGroup>
20+
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<UserControl
2+
xmlns="https://github.com/avaloniaui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
5+
xmlns:local="using:SharpFM.Plugin.XmlViewer"
6+
x:Class="SharpFM.Plugin.XmlViewer.XmlViewerPanel"
7+
x:DataType="local:XmlViewerViewModel">
8+
9+
<Grid RowDefinitions="Auto,*">
10+
<!-- Header -->
11+
<Border Grid.Row="0" Padding="12,8" Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">
12+
<TextBlock Classes="Fluent2Caption" Text="{Binding ClipLabel}" Opacity="0.8" />
13+
</Border>
14+
15+
<!-- XML Editor -->
16+
<AvaloniaEdit:TextEditor
17+
x:Name="xmlEditor"
18+
Grid.Row="1"
19+
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
20+
ShowLineNumbers="True"
21+
WordWrap="False"
22+
IsVisible="{Binding HasClip}"
23+
Document="{Binding Document}" />
24+
25+
<!-- Empty state -->
26+
<TextBlock
27+
Grid.Row="1"
28+
Classes="Fluent2Body"
29+
Opacity="0.5"
30+
HorizontalAlignment="Center"
31+
VerticalAlignment="Center"
32+
IsVisible="{Binding !HasClip}"
33+
Text="Select a clip to view its XML." />
34+
</Grid>
35+
36+
</UserControl>

0 commit comments

Comments
 (0)