Skip to content

Avalonia UI#975

Open
Deterous wants to merge 36 commits into
SabreTools:masterfrom
whatever-industries:avalonia
Open

Avalonia UI#975
Deterous wants to merge 36 commits into
SabreTools:masterfrom
whatever-industries:avalonia

Conversation

@Deterous
Copy link
Copy Markdown
Member

@Deterous Deterous commented May 6, 2026

This PR adds an alternate UI framework for MPF based on Avalonia, allowing for Linux and macOS UI builds.
There may be some small tweaks needed before merging during my testing, but I want to open the PR now to begin the review process as this is a big change.
Thanks to 7 (@whatev-indus) for putting in the work on this, I've just helped with guidance and then cleaned it up to use SabreTools styles and build processes. I will also be responding to any PR review comments. If you want to make changes to the branch directly and cannot, ask 7 for contributor access to the repo.

This does not replace the WPF UI, currently both are built separately. The images (icon, ringcode pics) are referenced from the WPF folder, but the Strings XAML are duplicated. The new MPF.Avalonia namespace is intended to reference the MPF.Frontend namespace as much as possible, so that it can be a drop-in replacement for the old WPF UI.

It is also 7's request to have AppImage builds for Linux and other build changes required for icons and niceties on macOS, although these changes can come in a later PR (to minimize the review burden)

@Deterous Deterous marked this pull request as draft May 6, 2026 01:31
@Deterous Deterous requested a review from mnadareski May 6, 2026 03:58
Copy link
Copy Markdown
Member

@mnadareski mnadareski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the inline comments pertain to multiple locations, but there were a couple of overarching comments that I wanted to mention as well:

  • There are basically no code comments. For anything that was based on existing WPF code, please copy the comment wholesale. For anything that is new functionality, add at the bare minimum a summary statement about what it is doing. I know that many are self-explanatory once you read the method, but those comments are used to get an understanding while coding to avoid having to look at the source for all details.
  • All loops, such as for, foreach, and while should have curly braces, regardless of the number of lines that are contained within.
  • I am seeing the use of global:: in many places and I would like to understand more about why this pattern is being used.
  • Unless there is ambiguity about what method or property is being invoked, you can avoid use of the this. qualifier.
  • There are many places, such as large lists of constants/variables or OS-specific methods, that could do with #region tags. This can help break up the code into more distinct sections and allows collapsing away logical groupings.

Comment thread MPF.Frontend/Drive.cs
/// <summary>
/// UI-friendly display name for the drive
/// </summary>
public string DisplayName => GetDisplayName();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be combined with the helper method into a cached property that utilizes the field keyword.

Comment thread MPF.Frontend/Drive.cs Outdated
DriveFormat = driveInfo.DriveFormat;
TotalSize = driveInfo.TotalSize;
VolumeLabel = driveInfo.VolumeLabel;
if (Environment.OSVersion.Platform != PlatformID.Unix)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is specific to Windows, then I'd prefer the check to say that instead. If saying "non-Unix" is actually the right way of doing something like Windows and Mac only, I would appreciate a one-line comment above this if/else that says that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that this is effectively Windows-only. DriveInfo.VolumeLabel is only available on Windows, and modern .NET reports both Linux and macOS as PlatformID.Unix, so != PlatformID.Unix resolves to "Windows" here. I didn't switch to OperatingSystem.IsWindows() because this project also targets net20-net48, where that helper doesn't exist. I've added a comment above the if/else explaining exactly that (commit 548f0ff).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to make the check explicit (commit b76b4cb): switched != PlatformID.Unix to == PlatformID.Win32NT, which reads as "Windows" directly as you preferred. I used PlatformID.Win32NT rather than OperatingSystem.IsWindows() because that helper only exists on net5.0+, and this project also targets net20-net48; PlatformID.Win32NT is available on all of them. The comment is now a single line noting that DriveInfo.VolumeLabel is Windows-only.

Comment thread MPF.Frontend/Drive.cs
Comment thread MPF.Frontend/Drive.cs
Comment on lines +270 to +271
if (displayName.Length > 1)
displayName = displayName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this has to be gated by a length check

Comment thread MPF.Avalonia/Program.cs
Comment on lines +193 to +196
"L0MasteringRing", "L0MasteringSID", "L0Toolstamp", "L0MouldSID", "L0AdditionalMould",
"L1MasteringRing", "L1MasteringSID", "L1Toolstamp", "L1MouldSID", "L1AdditionalMould",
"L2MasteringRing", "L2MasteringSID", "L2Toolstamp",
"L3MasteringRing", "L3MasteringSID", "L3Toolstamp",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it should be a static readonly collection at the class level. I have been thrown off seeing this in the declaration of a foreach statement multiple times now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (commit 536e1ee) — extracted to static readonly class-level arrays so they're no longer declared inline in the foreach: TabEnabledFieldNames here, and LanguageMenuItemNames for the equivalent block in MainWindow.

{
private readonly bool _showPcMacHybridAlways;

private static readonly (string Name, SiteCode Code)[] CommentFields =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these arrays of tuples instead of a dictionary?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're kept as ordered arrays because display order matters — the comment/content fields render in a fixed sequence, and a Dictionary doesn't guarantee enumeration order, so switching would risk shuffling the UI. They're also small, fixed lists iterated linearly, so a dictionary's lookup benefit doesn't really apply here. If keyed lookup is needed elsewhere I could add a dictionary alongside, but the ordered array matches how they're consumed.

Comment on lines +743 to +744
parameters = Regex.Replace(parameters, $@"(^|\s)--drive=(?:""{escapedDrive}/?""|{escapedDrive}/?)(?=\s|$)", "$1");
parameters = Regex.Replace(parameters, $@"(^|\s)--drive\s+(?:""{escapedDrive}/?""|{escapedDrive}/?)(?=\s|$)", "$1");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drive names need to be escaped for Redumper? I'm having a hard time following what these regex statements are actually doing to the resulting parameters.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS the optical drive mounts under /Volumes/, and Redumper there doesn't take the --drive argument the way it does on Windows. These statements strip any --drive=<vol> / --drive <vol> matching the mounted volume back out of the parameter string. Regex.Escape(driveName) is only there to make the volume name match literally — mounted volume names can contain spaces or regex metacharacters, so without escaping the pattern could misbehave. I can add an inline comment spelling that out.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the inline comments spelling this out (commit 548f0ff): one note on Regex.Escape (mounted volume names under /Volumes/ can contain spaces or regex metacharacters, so the name is escaped to match literally), and one on the two Regex.Replace calls (they strip the mounted drive's --drive=<name> / --drive <name> argument — quoted or unquoted, optional trailing slash — because Redumper on macOS doesn't take the drive argument the way it does on Windows).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear on the two parts: the Regex.Escape and what the Regex.Replace calls do are as described above (escaping is only to match the volume name literally in the .NET regex; the replaces strip the mounted-volume --drive argument). The underlying reason the argument is stripped specifically on macOS — i.e. exactly how Redumper expects the drive to be specified there — is best confirmed by @Deterous, who authored this method; my explanation of that part is an inference rather than something I can state authoritatively.

Comment on lines +164 to +169
private static NativeMenuItem CreateNativeMenuItem(string header, Action action)
{
var item = new NativeMenuItem { Header = CleanMenuHeader(header) };
item.Click += (_, _) => action();
return item;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a similar vein to the WireEvents methods, is there a reason that what is being passed in isn't just a Func or Action with the correct parameters for the event handler?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are delegates — the parameters are named delegate types rather than Func/Action, but functionally the same. The view model takes these callbacks (log output, user-message display, media-info window) so it can drive those UI concerns without depending on the View layer, keeping the MVVM separation. If the preference is to type them as Func<>/Action<> directly, I can switch the signatures.

{
public partial class MainWindow : WindowBase
{
private const double ExpandedLogPanelExtraHeight = 40;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit surprised that dimensions are allowed to have fractional values in Avalonia.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avalonia measures layout in device-independent pixels as double throughout (Width/Height/Margin etc. are all double), so fractional values are expected rather than special. The constant here is a whole number — it's just typed as double to match the layout API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants