From 051b7366c4ca735ecd62068d0966e4aeb1870302 Mon Sep 17 00:00:00 2001 From: wavever Date: Fri, 30 Oct 2026 16:10:37 +0800 Subject: [PATCH 1/2] fix(macos): preserve native window class to prevent quit crash --- src/Filee.Platform.MacOS/NativeMethods.cs | 20 +-- .../Filee.Platform.Tests/MacOSWindowTests.cs | 130 ++++++++++++++++++ 2 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 tests/Filee.Platform.Tests/MacOSWindowTests.cs diff --git a/src/Filee.Platform.MacOS/NativeMethods.cs b/src/Filee.Platform.MacOS/NativeMethods.cs index b343bab75..5c284f62a 100644 --- a/src/Filee.Platform.MacOS/NativeMethods.cs +++ b/src/Filee.Platform.MacOS/NativeMethods.cs @@ -131,17 +131,9 @@ internal static void RaiseTopmost(nint window) behavior &= ~((1 << 16) | (1 << 17) | (1 << 18)); // Finder can occupy a full-screen Space that would otherwise hide another app's toolbar. SendInteger(window, sel_registerName("setCollectionBehavior:"), behavior); - // While Filee shows in the Dock, AppKit keeps its windows off another app's full-screen Space and lets only a - // non-activating panel join it (#251). AvnPanel is Avalonia's NSPanel build of AvnWindow, same ivars and methods - // but canBecomeMainWindow, so the donut becomes one in place. - var panel = objc_getClass("AvnPanel"); - if (panel != 0 && object_getClass(window) == objc_getClass("AvnWindow")) - { - object_setClass(window, panel); - const nint nonactivatingPanel = 1 << 7; - SendInteger(window, sel_registerName("setStyleMask:"), Send(window, sel_registerName("styleMask")) | nonactivatingPanel); - SendInteger(window, sel_registerName("setHidesOnDeactivate:"), 0); - } + // Keep the native class selected when Avalonia created the window. Changing an initialized AvnWindow into + // AvnPanel skips NSPanel initialization and breaks AppKit's KVO cleanup when the window is released (#805). + // A non-activating panel for another app's full-screen Space (#251) must be created as a panel from the start. SendInteger(window, sel_registerName("setLevel:"), 3); SendVoid(window, sel_registerName("orderFrontRegardless")); } @@ -426,12 +418,6 @@ internal static bool RequestAccessibility() [DllImport(ObjC)] private static extern nint objc_getClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name); - [DllImport(ObjC)] - private static extern nint object_getClass(nint instance); - - [DllImport(ObjC)] - private static extern nint object_setClass(nint instance, nint cls); - [DllImport(ObjC)] private static extern nint sel_registerName([MarshalAs(UnmanagedType.LPUTF8Str)] string name); diff --git a/tests/Filee.Platform.Tests/MacOSWindowTests.cs b/tests/Filee.Platform.Tests/MacOSWindowTests.cs new file mode 100644 index 000000000..956fb2247 --- /dev/null +++ b/tests/Filee.Platform.Tests/MacOSWindowTests.cs @@ -0,0 +1,130 @@ +// Raising an existing window must preserve its native class and avoid panel-only configuration (#805). +// Objective-C test doubles exercise the real interop calls without creating AppKit UI on xUnit's worker thread. + +using System.Runtime.InteropServices; +using Filee.Platform.MacOS; + +namespace Filee.Platform.Tests; + +public sealed class MacOSWindowTests +{ + [Fact] + public void Raising_a_window_repeatedly_preserves_its_native_class_and_space_behavior() + { + if (!OperatingSystem.IsMacOS()) + { + Assert.Skip("Requires the macOS Objective-C runtime."); + return; + } + + _ = NativeLibrary.Load("/System/Library/Frameworks/Foundation.framework/Foundation"); + // Production checks these exact Avalonia class names. This test runs in the platform-only test process, + // which does not load Avalonia.Native; never replace a real class if another host has loaded it. + if (GetClass("AvnWindow") != 0 || GetClass("AvnPanel") != 0) + { + Assert.Skip("Requires a process without Avalonia.Native window classes."); + return; + } + + nint behavior = (1 << 1) | (1 << 6) | (1 << 7) | (1 << 9) | (1 << 16) | (1 << 17) | (1 << 18); + nint level = 0; + var orders = 0; + var panelConfigurationCalls = 0; + GetInteger getBehavior = (_, _) => behavior; + GetInteger getStyle = (_, _) => 0; + SetInteger setBehavior = (_, _, value) => behavior = value; + SetInteger setLevel = (_, _, value) => level = value; + SetInteger configurePanel = (_, _, _) => panelConfigurationCalls++; + SendAction orderFront = (_, _) => orders++; + Delegate[] callbacks = [getBehavior, getStyle, setBehavior, setLevel, configurePanel, orderFront]; + + var baseClass = AllocateClass(GetClass("NSObject"), "FileeRaiseTopmostTestWindow", 0); + Assert.NotEqual(0, baseClass); + nint windowClass = 0, panelClass = 0, window = 0; + try + { + AddMethod(baseClass, "collectionBehavior", getBehavior, "q@:"); + AddMethod(baseClass, "styleMask", getStyle, "q@:"); + AddMethod(baseClass, "setCollectionBehavior:", setBehavior, "v@:q"); + AddMethod(baseClass, "setLevel:", setLevel, "v@:q"); + AddMethod(baseClass, "setStyleMask:", configurePanel, "v@:q"); + AddMethod(baseClass, "setHidesOnDeactivate:", configurePanel, "v@:q"); + AddMethod(baseClass, "orderFrontRegardless", orderFront, "v@:"); + RegisterClass(baseClass); + windowClass = RegisterSubclass(baseClass, "AvnWindow"); + panelClass = RegisterSubclass(baseClass, "AvnPanel"); + window = Send(Send(windowClass, Selector("alloc")), Selector("init")); + Assert.NotEqual(0, window); + + var platform = new MacOSPlatformServices(); + for (var i = 1; i <= 3; i++) + { + platform.RaiseTopmost(window); + Assert.Equal(windowClass, ObjectClass(window)); + Assert.Equal((nint)(1 | (1 << 6) | (1 << 8)), behavior); + Assert.Equal((nint)3, level); + Assert.Equal(i, orders); + Assert.Equal(0, panelConfigurationCalls); + } + } + finally + { + if (window != 0) + Send(window, Selector("release")); + if (panelClass != 0) + DisposeClass(panelClass); + if (windowClass != 0) + DisposeClass(windowClass); + DisposeClass(baseClass); + GC.KeepAlive(callbacks); + } + } + + private static nint RegisterSubclass(nint superclass, string name) + { + var result = AllocateClass(superclass, name, 0); + Assert.NotEqual(0, result); + RegisterClass(result); + return result; + } + + private static void AddMethod(nint cls, string name, Delegate callback, string signature) => + Assert.True(AddNativeMethod(cls, Selector(name), Marshal.GetFunctionPointerForDelegate(callback), signature)); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate nint GetInteger(nint receiver, nint selector); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetInteger(nint receiver, nint selector, nint value); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SendAction(nint receiver, nint selector); + + private const string ObjC = "/usr/lib/libobjc.A.dylib"; + + [DllImport(ObjC, EntryPoint = "objc_getClass")] + private static extern nint GetClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name); + + [DllImport(ObjC, EntryPoint = "objc_allocateClassPair")] + private static extern nint AllocateClass(nint superclass, [MarshalAs(UnmanagedType.LPUTF8Str)] string name, nuint extraBytes); + + [DllImport(ObjC, EntryPoint = "objc_registerClassPair")] + private static extern void RegisterClass(nint cls); + + [DllImport(ObjC, EntryPoint = "objc_disposeClassPair")] + private static extern void DisposeClass(nint cls); + + [DllImport(ObjC, EntryPoint = "object_getClass")] + private static extern nint ObjectClass(nint instance); + + [DllImport(ObjC, EntryPoint = "class_addMethod")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool AddNativeMethod(nint cls, nint selector, nint implementation, + [MarshalAs(UnmanagedType.LPUTF8Str)] string signature); + + [DllImport(ObjC, EntryPoint = "sel_registerName")] + private static extern nint Selector([MarshalAs(UnmanagedType.LPUTF8Str)] string name); + + [DllImport(ObjC, EntryPoint = "objc_msgSend")] + private static extern nint Send(nint receiver, nint selector); +} From 825ecbd765713bda97c62e28165c289849e3783b Mon Sep 17 00:00:00 2001 From: wavever Date: Fri, 30 Oct 2026 22:30:46 +0800 Subject: [PATCH 2/2] fix(macos): restore donut window class on hide and before close --- src/Filee.App/Services/RadialController.cs | 10 +++ src/Filee.Core/Platform/IPlatformServices.cs | 3 + .../MacOSPlatformServices.cs | 7 ++ src/Filee.Platform.MacOS/NativeMethods.cs | 34 +++++++- .../Filee.App.Tests/RadialControllerTests.cs | 83 ++++++++++++++++++ .../Filee.Platform.Tests/MacOSWindowTests.cs | 87 ++++++++++++++++--- 6 files changed, 208 insertions(+), 16 deletions(-) diff --git a/src/Filee.App/Services/RadialController.cs b/src/Filee.App/Services/RadialController.cs index 05ca6259e..ffb441ad3 100644 --- a/src/Filee.App/Services/RadialController.cs +++ b/src/Filee.App/Services/RadialController.cs @@ -225,6 +225,16 @@ private RadialWindow EnsureWindow() var window = new RadialWindow { DataContext = viewModel }; var donut = window.DonutControl; + // macOS uses a panel while the donut is shown over a full-screen app. Restore its original window class + // after any hide and before Avalonia releases it at quit, including a quit while it is still visible (#805). + void RestoreNativeWindow() => platform.RestoreWindow(window.TryGetPlatformHandle()?.Handle ?? 0); + window.Closing += (_, _) => RestoreNativeWindow(); + window.PropertyChanged += (_, change) => + { + if (change.Property == RadialWindow.IsVisibleProperty && !change.GetNewValue()) + RestoreNativeWindow(); + }; + // On native Wayland, hiding an xdg_toplevel surface causes the compositor to destroy its surface, // and re-showing raises an InvalidOperationException. Track Closed to re-create on the next gesture. if (Filee.Platform.Linux.WaylandSupport.NativeWaylandRequested) diff --git a/src/Filee.Core/Platform/IPlatformServices.cs b/src/Filee.Core/Platform/IPlatformServices.cs index f42457f58..b999edcab 100644 --- a/src/Filee.Core/Platform/IPlatformServices.cs +++ b/src/Filee.Core/Platform/IPlatformServices.cs @@ -151,6 +151,9 @@ void OpenWith(string path) { } /// window below normal ones (seen after the Photos app opened) while it still reports itself as topmost. /// void RaiseTopmost(nint windowHandle); + + /// Restores temporary native window configuration after a raised window hides or before it closes. + void RestoreWindow(nint windowHandle) { } } /// diff --git a/src/Filee.Platform.MacOS/MacOSPlatformServices.cs b/src/Filee.Platform.MacOS/MacOSPlatformServices.cs index 30c923b9b..100bedad4 100644 --- a/src/Filee.Platform.MacOS/MacOSPlatformServices.cs +++ b/src/Filee.Platform.MacOS/MacOSPlatformServices.cs @@ -129,6 +129,13 @@ public void RaiseTopmost(nint windowHandle) NativeMethods.RaiseTopmost(windowHandle); } + /// Restores a topmost window's native class and style after it hides or before it closes. + public void RestoreWindow(nint windowHandle) + { + if (windowHandle != 0) + NativeMethods.RestoreWindow(windowHandle); + } + /// /// Lets drags that offer file paths or file promises in older forms reach the window (the donut), not just ones /// offering file URLs: other file managers than Finder write those (issue #160). diff --git a/src/Filee.Platform.MacOS/NativeMethods.cs b/src/Filee.Platform.MacOS/NativeMethods.cs index 5c284f62a..8fe14deb1 100644 --- a/src/Filee.Platform.MacOS/NativeMethods.cs +++ b/src/Filee.Platform.MacOS/NativeMethods.cs @@ -131,13 +131,35 @@ internal static void RaiseTopmost(nint window) behavior &= ~((1 << 16) | (1 << 17) | (1 << 18)); // Finder can occupy a full-screen Space that would otherwise hide another app's toolbar. SendInteger(window, sel_registerName("setCollectionBehavior:"), behavior); - // Keep the native class selected when Avalonia created the window. Changing an initialized AvnWindow into - // AvnPanel skips NSPanel initialization and breaks AppKit's KVO cleanup when the window is released (#805). - // A non-activating panel for another app's full-screen Space (#251) must be created as a panel from the start. + // A non-activating panel can join another app's full-screen Space while Filee remains in the Dock (#251). + // Restore AvnWindow when hidden and before close so AppKit's KVO cleanup sees the initialized class (#805). + var panel = objc_getClass("AvnPanel"); + if (panel != 0 && object_getClass(window) == objc_getClass("AvnWindow")) + { + object_setClass(window, panel); + const nint nonactivatingPanel = 1 << 7; + SendInteger(window, sel_registerName("setStyleMask:"), Send(window, sel_registerName("styleMask")) | nonactivatingPanel); + SendInteger(window, sel_registerName("setHidesOnDeactivate:"), 0); + } SendInteger(window, sel_registerName("setLevel:"), 3); SendVoid(window, sel_registerName("orderFrontRegardless")); } + internal static void RestoreWindow(nint window) + { + if (window == 0) + return; + + var panel = objc_getClass("AvnPanel"); + var avnWindow = objc_getClass("AvnWindow"); + if (panel == 0 || avnWindow == 0 || object_getClass(window) != panel) + return; + + const nint nonactivatingPanel = 1 << 7; + SendInteger(window, sel_registerName("setStyleMask:"), Send(window, sel_registerName("styleMask")) & ~nonactivatingPanel); + object_setClass(window, avnWindow); + } + /// /// Adds to the drag types of every view in that already takes /// drags (Avalonia's view, registered for public.data only). AppKit sends a drag to a view only when it offers one @@ -418,6 +440,12 @@ internal static bool RequestAccessibility() [DllImport(ObjC)] private static extern nint objc_getClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name); + [DllImport(ObjC)] + private static extern nint object_getClass(nint instance); + + [DllImport(ObjC)] + private static extern nint object_setClass(nint instance, nint cls); + [DllImport(ObjC)] private static extern nint sel_registerName([MarshalAs(UnmanagedType.LPUTF8Str)] string name); diff --git a/tests/Filee.App.Tests/RadialControllerTests.cs b/tests/Filee.App.Tests/RadialControllerTests.cs index ede1551c7..8bd650714 100644 --- a/tests/Filee.App.Tests/RadialControllerTests.cs +++ b/tests/Filee.App.Tests/RadialControllerTests.cs @@ -7,12 +7,95 @@ using Avalonia.Input; using Filee.App.Controls; using Filee.App.Services; +using Filee.App.Services.Triggers; +using Filee.App.ViewModels; +using Filee.Core.Platform; using Filee.Core.Presets; +using Filee.Core.Settings; +using Microsoft.Extensions.Logging.Abstractions; namespace Filee.App.Tests; public class RadialControllerTests { + [AvaloniaFact] + public void Hiding_the_donut_restores_native_configuration_before_it_is_reused() + { + TestServices.EnsureInitialized("en"); + var platform = new WindowPlatform(); + var radial = Create(platform); + try + { + for (var i = 1; i <= 3; i++) + { + radial.ShowForDrag(400, 400); + Assert.Equal(i - 1, platform.Restores); + // Both an external Hide and the controller's close path must restore the native window. + if (i == 1) + radial.Window!.Hide(); + else + radial.Close(animate: false); + Assert.False(radial.Window!.IsVisible); + Assert.Equal(i, platform.Restores); + } + } + finally + { + radial.Window?.Close(); + } + } + + [AvaloniaFact] + public void Closing_a_visible_donut_restores_it_before_the_native_window_is_released() + { + TestServices.EnsureInitialized("en"); + var platform = new WindowPlatform(); + var radial = Create(platform); + radial.ShowForDrag(400, 400); + var window = radial.Window!; + var checkedBeforeRelease = false; + window.Closing += (_, _) => + { + Assert.Equal(1, platform.Restores); + Assert.NotNull(window.PlatformImpl); + checkedBeforeRelease = true; + }; + try + { + window.Close(); + Assert.True(checkedBeforeRelease); + Assert.Null(window.PlatformImpl); + } + finally + { + if (window.PlatformImpl is not null) + window.Close(); + } + } + + private static RadialController Create(IPlatformServices platform) => + new(AppHost.Get(), AppHost.Get(), AppHost.Get(), + AppHost.Get(), platform, AppHost.Get(), NullLogger.Instance); + + private sealed class WindowPlatform : IPlatformServices + { + public int Restores { get; private set; } + public void RestoreWindow(nint windowHandle) => Restores++; + public void RaiseTopmost(nint windowHandle) { } + public bool IsFileManagerAt(int x, int y) => false; + public string? ProcessNameAt(int x, int y) => null; + public IReadOnlyList GetFileManagerSelection() => []; + public int SystemDragThreshold => 4; + public bool PrefersReducedMotion => false; + public void SetStartWithSystem(bool enabled, string executablePath) { } + public void SetContextMenu(bool enabled, string executablePath, string label) { } + public ModernContextMenuState GetModernContextMenuState(string executablePath) => ModernContextMenuState.Unsupported; + public Task SetModernContextMenuAsync(bool enabled, string executablePath) => + Task.FromResult(new ModernContextMenuResult(false)); + public void RevealInFileManager(string path) { } + public void OpenFile(string path) { } + } + /// A conversion ran through the donut and its ring finished: progress mode stays on, nothing is pending. private static void FinishARing(DonutMenu donut) { diff --git a/tests/Filee.Platform.Tests/MacOSWindowTests.cs b/tests/Filee.Platform.Tests/MacOSWindowTests.cs index 956fb2247..fbbc688fa 100644 --- a/tests/Filee.Platform.Tests/MacOSWindowTests.cs +++ b/tests/Filee.Platform.Tests/MacOSWindowTests.cs @@ -1,4 +1,4 @@ -// Raising an existing window must preserve its native class and avoid panel-only configuration (#805). +// Topmost windows use a panel while shown, then restore their original class/style when hidden or before close (#805). // Objective-C test doubles exercise the real interop calls without creating AppKit UI on xUnit's worker thread. using System.Runtime.InteropServices; @@ -9,7 +9,7 @@ namespace Filee.Platform.Tests; public sealed class MacOSWindowTests { [Fact] - public void Raising_a_window_repeatedly_preserves_its_native_class_and_space_behavior() + public void Raising_and_restoring_a_window_repeatedly_restores_its_native_class_and_style() { if (!OperatingSystem.IsMacOS()) { @@ -27,50 +27,111 @@ public void Raising_a_window_repeatedly_preserves_its_native_class_and_space_beh } nint behavior = (1 << 1) | (1 << 6) | (1 << 7) | (1 << 9) | (1 << 16) | (1 << 17) | (1 << 18); + const nint nonactivatingPanel = 1 << 7; + const nint unrelatedStyle = (1 << 2) | (1 << 11); + nint style = unrelatedStyle; nint level = 0; var orders = 0; - var panelConfigurationCalls = 0; + var hidesOnDeactivate = true; + var styleChanges = 0; + var hidesOnDeactivateChanges = 0; GetInteger getBehavior = (_, _) => behavior; - GetInteger getStyle = (_, _) => 0; + GetInteger getStyle = (_, _) => style; SetInteger setBehavior = (_, _, value) => behavior = value; SetInteger setLevel = (_, _, value) => level = value; - SetInteger configurePanel = (_, _, _) => panelConfigurationCalls++; + SetInteger setStyle = (_, _, value) => + { + style = value; + styleChanges++; + }; + SetInteger setHidesOnDeactivate = (_, _, value) => + { + hidesOnDeactivate = value != 0; + hidesOnDeactivateChanges++; + }; SendAction orderFront = (_, _) => orders++; - Delegate[] callbacks = [getBehavior, getStyle, setBehavior, setLevel, configurePanel, orderFront]; + Delegate[] callbacks = [getBehavior, getStyle, setBehavior, setLevel, setStyle, setHidesOnDeactivate, orderFront]; var baseClass = AllocateClass(GetClass("NSObject"), "FileeRaiseTopmostTestWindow", 0); Assert.NotEqual(0, baseClass); - nint windowClass = 0, panelClass = 0, window = 0; + nint windowClass = 0, panelClass = 0, window = 0, foreign = 0; try { AddMethod(baseClass, "collectionBehavior", getBehavior, "q@:"); AddMethod(baseClass, "styleMask", getStyle, "q@:"); AddMethod(baseClass, "setCollectionBehavior:", setBehavior, "v@:q"); AddMethod(baseClass, "setLevel:", setLevel, "v@:q"); - AddMethod(baseClass, "setStyleMask:", configurePanel, "v@:q"); - AddMethod(baseClass, "setHidesOnDeactivate:", configurePanel, "v@:q"); + AddMethod(baseClass, "setStyleMask:", setStyle, "v@:q"); + AddMethod(baseClass, "setHidesOnDeactivate:", setHidesOnDeactivate, "v@:q"); AddMethod(baseClass, "orderFrontRegardless", orderFront, "v@:"); RegisterClass(baseClass); + + var platform = new MacOSPlatformServices(); + platform.RaiseTopmost(0); + platform.RestoreWindow(0); + foreign = Send(Send(baseClass, Selector("alloc")), Selector("init")); + Assert.NotEqual(0, foreign); + platform.RaiseTopmost(foreign); + platform.RestoreWindow(foreign); + Assert.Equal(baseClass, ObjectClass(foreign)); + Assert.Equal(unrelatedStyle, style); + Assert.True(hidesOnDeactivate); + Assert.Equal(0, styleChanges); + Assert.Equal(0, hidesOnDeactivateChanges); + orders = 0; + windowClass = RegisterSubclass(baseClass, "AvnWindow"); panelClass = RegisterSubclass(baseClass, "AvnPanel"); window = Send(Send(windowClass, Selector("alloc")), Selector("init")); Assert.NotEqual(0, window); - var platform = new MacOSPlatformServices(); for (var i = 1; i <= 3; i++) { platform.RaiseTopmost(window); - Assert.Equal(windowClass, ObjectClass(window)); + Assert.Equal(panelClass, ObjectClass(window)); Assert.Equal((nint)(1 | (1 << 6) | (1 << 8)), behavior); + Assert.Equal(unrelatedStyle | nonactivatingPanel, style); + Assert.False(hidesOnDeactivate); Assert.Equal((nint)3, level); - Assert.Equal(i, orders); - Assert.Equal(0, panelConfigurationCalls); + Assert.Equal(2 * i - 1, orders); + Assert.Equal(2 * i - 1, styleChanges); + Assert.Equal(i, hidesOnDeactivateChanges); + + platform.RaiseTopmost(window); + Assert.Equal(panelClass, ObjectClass(window)); + Assert.Equal(unrelatedStyle | nonactivatingPanel, style); + Assert.False(hidesOnDeactivate); + Assert.Equal(2 * i, orders); + Assert.Equal(2 * i - 1, styleChanges); + Assert.Equal(i, hidesOnDeactivateChanges); + + platform.RestoreWindow(window); + Assert.Equal(windowClass, ObjectClass(window)); + Assert.Equal(unrelatedStyle, style); + Assert.False(hidesOnDeactivate); + Assert.Equal(2 * i, styleChanges); + + platform.RestoreWindow(window); + Assert.Equal(windowClass, ObjectClass(window)); + Assert.Equal(unrelatedStyle, style); + Assert.Equal(2 * i, styleChanges); } + + var styleChangesBeforeForeignRestore = styleChanges; + var hidesOnDeactivateChangesBeforeForeignRestore = hidesOnDeactivateChanges; + platform.RaiseTopmost(foreign); + platform.RestoreWindow(foreign); + Assert.Equal(baseClass, ObjectClass(foreign)); + Assert.Equal(unrelatedStyle, style); + Assert.Equal(styleChangesBeforeForeignRestore, styleChanges); + Assert.Equal(hidesOnDeactivateChangesBeforeForeignRestore, hidesOnDeactivateChanges); } finally { if (window != 0) Send(window, Selector("release")); + if (foreign != 0) + Send(foreign, Selector("release")); if (panelClass != 0) DisposeClass(panelClass); if (windowClass != 0)