Unreal Engine Archives Explorer
Go to file
Rob Trame c33d5f58f2
[P2-007] Migrate Magnifier adorner system to Avalonia (#74)
* feat(P2-007): migrate Magnifier adorner system to Avalonia (#20)

Magnifier.cs:
- System.Windows.Controls.Control → Avalonia TemplatedControl
- DependencyProperty → StyledProperty<T> with AddClassHandler callbacks
- Target property type UIElement → Control (Avalonia uses Control as the
  visual-tree base)
- ZoomFactor / ZoomFactorOnMouseWheel: validation callback replaced with
  validate: parameter on Register (Avalonia 11 API)
- OverrideMetadata for Width/Height → OverrideDefaultValue<T>
- SizeChanged event → BoundsProperty.Changed class handler
- IsInitialized guard → Bounds.Width/Height > 0 guard
- Remove PART_VisualBrush TemplateBinding lookup: Magnifier now renders
  itself via Render(DrawingContext) instead of relying on a ControlTemplate
  in Resources.xaml (which is not yet migrated). Circle/Rectangle shapes
  are drawn directly via DrawingContext.DrawEllipse / DrawRectangle using
  a VisualBrush whose SourceControl points at the Target; SourceRect
  carries the viewport (equivalent to WPF VisualBrush.Viewbox / Absolute
  ViewboxUnits). Brush is rebuilt in RebuildBrush whenever Target changes.
- Avalonia.Data using removed (unused after DependencyProperty removal)

MagnifierAdorner.cs:
- WPF Adorner subclass replaced by Canvas-based adorner control; the
  canvas is placed in the AdornerLayer via AdornerLayer.SetAdornment
  (called by MagnifierManager).
- AddVisualChild(_magnifier) / ArrangeOverride replaced by Canvas.Children
  plus Canvas.SetLeft/SetTop for positioning.
- InputManager.Current.PostProcessInput replaced by
  PointerMoved on the adorned element; position is obtained via
  e.GetPosition(this) relative to the adorner canvas.
- VisualTreeHelper.GetOffset → Control.TranslatePoint to get the
  Target's offset within the adorned element's coordinate space.
- Detach() method added to allow clean unsubscription on removal.

MagnifierManager.cs:
- DependencyObject with RegisterAttached → plain class with
  AvaloniaProperty.RegisterAttached; change handler wired via
  MagnifierProperty.Changed.Subscribe.
- MouseLeftButtonDown/Up → PointerPressed/Released events.
- MouseWheel → PointerWheelChanged; WPF Delta sign convention:
  Delta < 0 = scroll down = zoom out; Avalonia Delta.Y < 0 = scroll
  down — same directional mapping applied.
- AdornerLayer.GetAdornerLayer + layer.Add → AdornerLayer.GetAdornerLayer
  + AdornerLayer.SetAdornment (Avalonia 11 API).
- Visibility.Visible/Collapsed → IsVisible bool.

ImagePopout.xaml:
- AdonisWindow + WPF namespace → Avalonia Window.
- MagnifierManager.Magnifier inline object-element syntax removed;
  Magnifier is now attached in code-behind (XAML object-valued attached
  property on a different element is not supported in Avalonia).
- DockPanel given x:Name="RootPanel" for code-behind access.
- UseLayoutRounding removed (Avalonia default).

ImagePopout.xaml.cs:
- Added : Window base class and using Avalonia.Markup.Xaml.
- Magnifier attached in OnAttachedToVisualTree to avoid running before
  the adorner layer is available.

Closes #20

* Add untracked file

* fix(magnifier): address PR #74 review findings

C1/C2 (Magnifier.cs)
- BoundsProperty.Changed.AddClassHandler moved from instance ctor to
  static ctor; now also calls InvalidateVisual() (C1: was registering N
  handlers for N instances)
- TargetProperty.Changed.AddClassHandler added to static ctor so
  RebuildBrush fires whenever Target is reassigned (C2 / PR-4)
- Remove unused 'using Avalonia.Data' (m1 / PR-5)

C3/M2 (MagnifierAdorner.cs)
- Track _currentElementPosition (element-relative) alongside
  _currentPointerPosition (adorner-relative); both captured from the
  same PointerEventArgs so no PointToScreen/PointToClient round-trip
  is needed (C3)
- CalculateViewBoxLocation now uses _currentElementPosition; the old
  PointToClient(PointToScreen(...)) call is removed
- Subscribe to adornedElement.PointerPressed alongside PointerMoved;
  both route through a shared HandlePointerEvent() so the magnifier is
  pre-positioned before the first render frame (M2 / PR-3)
- Detach() now unsubscribes both PointerPressed and PointerMoved (PR-2)
- Remove unused 'using Avalonia.VisualTree' (PR-6)

M1/PR-1/PR-2 (MagnifierManager.cs)
- e.NewValue.GetValueOrDefault() replaces unsafe e.NewValue.Value
  access (PR-1)
- ConditionalWeakTable<Control, MagnifierManager> lets OnMagnifierChanged
  detach the old manager when the attached property is changed or cleared
- AttachToMagnifier subscribes element.DetachedFromVisualTree so the
  adorner is detached when the host element leaves the tree (M1 / PR-2)
- New Detach() instance method: unsubscribes all element events,
  calls adorner.Detach(), hides and nulls the adorner

ImagePopout.xaml.cs
- Remove unused 'using Avalonia.Markup.Xaml' (PR-7)
- Use compiler-generated RootPanel field directly instead of
  this.FindControl<DockPanel>("RootPanel") (m2)

* fix(magnifier): address review findings from PR #74

C1: Stretch.Fill in RebuildBrush — restores 1/ZoomFactor magnification
    ratio; Stretch.None was painting source at 1:1 with no zoom effect.

C2: Wire SetMagnifier in ImagePopout constructor, not OnAttachedToVisualTree.
    Window is the visual root so AttachedToVisualTree never fires on it;
    constructor wiring matches WPF parse-time property assignment timing.

M1: Filter PointerPressed/Released to left button only via PointerUpdateKind,
    matching WPF MouseLeftButtonDown/MouseLeftButtonUp routing event semantics.

M2: Remove dead OnApplyTemplate override; no ControlTheme is registered for
    Magnifier so the override could never be called.

S1: Change base class from TemplatedControl to Control; declare Background,
    BorderBrush and BorderThickness as local StyledProperty fields. Makes
    the template-less rendering path explicit at the type-system level.

Min1: Subscribe PointerMoved dynamically via OnPropertyChanged(IsVisibleProperty)
      so the handler is only live while the magnifier is visible, matching the
      WPF Loaded/Unloaded-gated InputManager.PostProcessInput subscription.

Min2: Guard BoundsProperty.Changed with an oldBounds.Size == newBounds.Size
      short-circuit to skip redundant UpdateViewBox calls on position-only
      changes (e.g. PositionMagnifier calling Canvas.SetLeft/SetTop).

Closes review findings from PR #74 / issue #20.

* fix(magnifier): address two unresolved PR review comments

MagnifierManager.OnElementDetached: call full Detach() + _managers.Remove()
  instead of only _adorner.Detach(). The partial cleanup left PointerPressed,
  PointerReleased, PointerWheelChanged and DetachedFromVisualTree subscriptions
  live on the element, causing stale handlers if the control re-entered the
  visual tree. Full teardown ensures a clean slate for any future re-attachment.

Magnifier.UpdateViewBox: fall back to Width/Height styled properties (default
  100px) when Bounds are still zero (pre-first-layout). This ensures ViewBox.Size
  is non-zero on the very first PointerPressed, so CalculateViewBoxLocation()
  correctly centers the viewport under the cursor before the initial measure pass
  has completed.

* fix(magnifier): address final review findings

Min1: Remove adorner from AdornerLayer on Detach() via SetAdornment(element, null)
  so the control is not left as an invisible orphan child in the layer for the
  lifetime of the host window. HideAdorner() call removed as redundant — layer
  removal is a stronger cleanup than a visibility toggle.

S1: Remove redundant InvalidateVisual() call in BoundsProperty.Changed handler.
  UpdateViewBox() already calls InvalidateVisual() at its end; the direct call
  in the handler was scheduling the same dirty-flag set twice per size change.

* fix(magnifier): address four unresolved PR review comments

MagnifierAdorner ctor: set IsVisible=false so the first ShowAdorner() call
  triggers a false→true transition on IsVisibleProperty, firing OnPropertyChanged
  and subscribing PointerMoved. Previously, IsVisible defaulted to true, so
  the first ShowAdorner() was a no-op and PointerMoved was never subscribed
  until after the first hide/show cycle.

MagnifierManager.Detach(): replace 'AdornerLayer.GetAdornerLayer + layer?.SetAdornment'
  with a direct 'AdornerLayer.SetAdornment(_element, null)' static call. In
  Avalonia 11 SetAdornment is static; calling it via an instance reference made
  the null-conditional '?.' meaningless and was inconsistent with VerifyAdornerLayer
  which correctly uses the static API. Add/remove are now symmetrical.

ImagePopout.xaml: remove unused xmlns:controls namespace alias. The magnifier
  wiring was moved to code-behind; no XAML element in this file references the
  alias, eliminating a linter warning.
2026-03-14 21:15:49 -06:00
.github chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
.vscode Set up dev env 2026-03-11 15:51:34 -06:00
CUE4Parse@06fbf1aced chore: point CUE4Parse submodule at r6e/CUE4Parse-Linux (#66) 2026-03-11 19:49:17 -06:00
FModel [P2-007] Migrate Magnifier adorner system to Avalonia (#74) 2026-03-14 21:15:49 -06:00
.editorconfig chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
.gitignore Finish setting up env 2026-03-11 17:28:01 -06:00
.gitmodules chore: point CUE4Parse submodule at r6e/CUE4Parse-Linux (#66) 2026-03-11 19:49:17 -06:00
LICENSE Re-add GPL-3 license 2021-05-22 16:33:08 -04:00
NOTICE chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
README.md chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00

FModel Linux — Unreal Engine Archives Explorer for Linux

CI Status Latest


This is an unofficial Linux port of FModel, originally created by Asval (4sval) and contributors. It is not affiliated with or endorsed by the upstream project or its maintainers. Please do not report Linux-port-specific issues upstream.

Description

FModel Linux is a Linux port of FModel — an archive explorer for Unreal Engine games. It uses CUE4Parse as its core parsing library, providing robust support for the latest UE4 and UE5 archive formats, along with a comprehensive set of tools for previewing and converting game packages.

This fork replaces the Windows-only WPF UI stack with Avalonia UI and removes other Windows-specific dependencies to enable native Linux support. It is maintained by r6e.

Installation

Installation instructions for the Linux port are available in the project wiki (work in progress).

For the upstream Windows release, refer to the official FModel installation guide.

Supporting the Upstream Project

This fork does not accept donations. If you find FModel valuable, please consider supporting the original FModel project and its contributors.

License

FModel Linux is a derivative work licensed under GPL-3. The original FModel project is copyright © Asval and FModel contributors. Licenses of third-party libraries used are listed in NOTICE.