Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,14 @@ wheel flips). In click mode the focused window gets the key (`DonutMenu.HandleKe
drag the donut's window has no keyboard focus, so `RadialController` turns on `TriggerService.WatchFlipKey` while
the drag donut shows files and flips on `FlipKeyPressed` / `FlipWheelTurned`: the global hook only listens (it
can't hold an event back; Explorer and Finder ignore Tab during a drag) and reports one flip per key press. On
macOS the drag donut takes the keyboard once files are on it (`RadialController.TakeKeyboard`), and the key reaches
its window as in click mode: while another app holds Secure Input (Terminal with Secure Keyboard Entry can keep it
from the background) macOS gives no keys to a hook, only to the app in front. The hook's copy of a press is ignored
while the window has the keyboard (`HookFlipsKey`, one flip per press, following `IsActive`); the keyboard goes back
to the app that had it once a conversion starts or the donut closes (`IPlatformServices.RememberFrontApp` /
`ReturnFocus`), unless the user clicked elsewhere meanwhile, and stays with Filee when the drop opens a window of its
own (a tool, the merge order, the save dialog). Not on Windows, where taking the keyboard during a drag breaks Explorer's.
macOS the drag donut takes key window status once files are on it (`RadialController.TakeKeyboard` / `IPlatformServices.TakeKeyWindow`),
and the key reaches its window without whole-application activation, keeping macOS Show Desktop (Desktop Reveal) and Exposé uninterrupted:
while another app holds Secure Input (Terminal with Secure Keyboard Entry can keep it from the background) macOS gives no keys to a hook,
only to the key window and app in front. The hook's copy of a press is ignored while the window has the keyboard (`HookFlipsKey`, one flip
per press, following `IsActive`); key window status is explicitly released once a conversion starts or the donut closes
(`IPlatformServices.ReleaseKeyWindow` / `ReturnFocus`), returning typing to the previous app (unless the user clicked elsewhere meanwhile) while the progress ring finishes, and stays
with Filee when the drop opens a window of its own (a tool, the merge order, the save dialog). Not on Windows, where taking the keyboard
during a drag breaks Explorer's.
The editor's Front / Back switch (`ToolbarPageViewModel.EditingBack`) edits either list with the same drag and drop.

A group is one slice that opens up to six non-group actions. Its members may be conversions or interactive tools
Expand Down
5 changes: 3 additions & 2 deletions src/Filee.App/Controls/DonutMenu.Flip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
// Click mode → the key reaches the focused window (HandleKey), the wheel the donut (OnPointerWheelChanged).
// Drag mode → the window has no keyboard focus during an OS drag: RadialController flips when the global hook
// sees the key or the wheel (TriggerService.FlipKeyPressed / FlipWheelTurned). On macOS the donut takes
// the keyboard once files are dragged onto it (RadialController.TakeKeyboard), and the key then reaches
// the window as in click mode: the hook may not see it while another app holds Secure Input.
// key window status without whole-app activation once files are dragged onto it (RadialController.TakeKeyboard),
// and the key then reaches the window as in click mode: the hook may not see it while another app holds
// Secure Input. On drop or close, key window status is explicitly released (ReleaseKeyWindow / ReturnFocus).
//
// The owner swaps Items immediately when FaceSwapping is raised (RadialWindow → RadialViewModel). Old slices are
// retained for drawing only; hit-testing and drops always use the new face.
Expand Down
21 changes: 14 additions & 7 deletions src/Filee.App/Services/RadialController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -745,12 +745,14 @@ private void WatchFlipKey()
private bool _tookKeyboard;

/// <summary>
/// macOS: once files are dragged onto the donut, it takes the keyboard (Filee comes to the front), so the flip key
/// reaches its window as a key press like any other. Watched from outside by the global hook it may not: while an
/// app holds Secure Input (Terminal with Secure Keyboard Entry, which can keep it from the background) macOS
/// gives no keys to anything watching, only to the app in front. The drag carries on; the keyboard goes back to the
/// app that had it once the conversion starts or the donut closes (<see cref="ReturnKeyboard"/>), and stays with
/// Filee when the drop opens a window of its own: a tool, the merge order, the save dialog (<see cref="KeepKeyboard"/>).
/// macOS: once files are dragged onto the donut, it takes the keyboard without activating the entire application
/// (by making the non-activating panel the key window), so the flip key reaches its window as a key press while
/// leaving macOS "Show Desktop" (Desktop Reveal) mode uninterrupted. Watched from outside by the global hook it
/// may not reach: while an app holds Secure Input (Terminal with Secure Keyboard Entry, which can keep it from the
/// background) macOS gives no keys to a global hook, only to the key window and app in front. The drag carries on;
/// the keyboard goes back to the app that had it once the conversion starts or the donut closes (<see cref="ReturnKeyboard"/>),
/// releasing key window status so typing returns to the previous app while the conversion ring finishes, and stays
/// with Filee when the drop opens a window of its own: a tool, the merge order, the save dialog (<see cref="KeepKeyboard"/>).
/// Not on Windows, where a window taking the keyboard during a drag breaks Explorer's drag.
/// </summary>
private void TakeKeyboard(RadialWindow window)
Expand All @@ -759,7 +761,10 @@ private void TakeKeyboard(RadialWindow window)
return;
platform.RememberFrontApp();
_tookKeyboard = true;
window.Activate();
var handle = window.TryGetPlatformHandle()?.Handle ?? 0;
platform.TakeKeyWindow(handle);
if (handle == 0)
window.Activate();
}

/// <summary>The drop opens a window of Filee's own: the keyboard stays with Filee for it, not given back on close.</summary>
Expand All @@ -771,6 +776,8 @@ private void ReturnKeyboard()
if (!_tookKeyboard)
return;
_tookKeyboard = false;
var handle = _window?.TryGetPlatformHandle()?.Handle ?? 0;
platform.ReleaseKeyWindow(handle);
platform.ReturnFocus();
}

Expand Down
11 changes: 11 additions & 0 deletions src/Filee.Core/Platform/IPlatformServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ void RememberFrontApp() { }
/// didn't click elsewhere meanwhile) and that app still runs; forgets it either way.
/// </summary>
void ReturnFocus() { }

/// <summary>
/// Makes the window key without activating the entire application, so the window can receive key presses
/// (such as the flip key during a drag) while leaving macOS Desktop Reveal / Exposé mode uninterrupted.
/// </summary>
void TakeKeyWindow(nint windowHandle) { }

/// <summary>
/// Resigns key window status on a window previously made key, returning key events to the frontmost application.
/// </summary>
void ReleaseKeyWindow(nint windowHandle) { }
}

/// <summary>
Expand Down
30 changes: 30 additions & 0 deletions src/Filee.Platform.MacOS/MacOSPlatformServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,19 @@ public void RaiseTopmost(nint windowHandle)
public void RestoreWindow(nint windowHandle)
{
if (windowHandle != 0)
{
if (_keyWindow == windowHandle)
{
_keyWindow = 0;
NativeMethods.ReleaseKeyWindow(windowHandle);
}
NativeMethods.RestoreWindow(windowHandle);
}
}

// The app the donut took the keyboard from (its process id), 0 for none.
private int _focusFrom;
private nint _keyWindow;

public void RememberFrontApp()
{
Expand All @@ -154,12 +162,34 @@ public void RememberFrontApp()

public void ReturnFocus()
{
var keyWindow = _keyWindow;
_keyWindow = 0;
if (keyWindow != 0)
NativeMethods.ReleaseKeyWindow(keyWindow);

var from = _focusFrom;
_focusFrom = 0;
if (from != 0 && NativeMethods.FrontmostProcessId() == Environment.ProcessId)
NativeMethods.ActivateProcess(from);
}

public void TakeKeyWindow(nint windowHandle)
{
if (windowHandle == 0)
return;
_keyWindow = windowHandle;
NativeMethods.TakeKeyWindow(windowHandle);
}

public void ReleaseKeyWindow(nint windowHandle)
{
if (windowHandle == 0)
return;
if (_keyWindow == windowHandle)
_keyWindow = 0;
NativeMethods.ReleaseKeyWindow(windowHandle);
}

/// <summary>
/// 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).
Expand Down
38 changes: 36 additions & 2 deletions src/Filee.Platform.MacOS/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ internal static bool PrefersReducedMotion()

internal static void RaiseTopmost(nint window)
{
const nint canJoinAllSpaces = 1, moveToActiveSpace = 1 << 1;
const nint canJoinAllSpaces = 1, moveToActiveSpace = 1 << 1, stationary = 1 << 4;
const nint fullScreenPrimary = 1 << 7, fullScreenAuxiliary = 1 << 8, fullScreenNone = 1 << 9;
var behavior = Send(window, sel_registerName("collectionBehavior"));
behavior = (behavior & ~(moveToActiveSpace | fullScreenPrimary | fullScreenNone)) | canJoinAllSpaces | fullScreenAuxiliary;
behavior = (behavior & ~(moveToActiveSpace | fullScreenPrimary | fullScreenNone)) | canJoinAllSpaces | stationary | fullScreenAuxiliary;
// macOS 13's primary, auxiliary and canJoinAllApplications: with canJoinAllApplications set the donut still
// stays off a full-screen Finder Space, even as a panel (#251).
behavior &= ~((1 << 16) | (1 << 17) | (1 << 18));
Expand Down Expand Up @@ -509,4 +509,38 @@ internal static bool RequestAccessibility()
[DllImport(ObjC, EntryPoint = "objc_msgSend")]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool SendBoolWithInteger(nint receiver, nint selector, nint value);

internal static void TakeKeyWindow(nint window)
{
var selector = sel_registerName("makeKeyAndOrderFront:");
if (SendBool(window, sel_registerName("respondsToSelector:"), selector))
SendVoid(window, selector, 0);
}

internal static void ReleaseKeyWindow(nint window)
{
if (!IsKeyWindow(window))
return;

var isVisibleSelector = sel_registerName("isVisible");
var wasVisible = SendBool(window, sel_registerName("respondsToSelector:"), isVisibleSelector)
&& SendBool(window, isVisibleSelector);

var orderOutSelector = sel_registerName("orderOut:");
if (SendBool(window, sel_registerName("respondsToSelector:"), orderOutSelector))
SendVoid(window, orderOutSelector, 0);

if (wasVisible)
{
var orderFrontSelector = sel_registerName("orderFrontRegardless");
if (SendBool(window, sel_registerName("respondsToSelector:"), orderFrontSelector))
SendVoid(window, orderFrontSelector);
}
}

internal static bool IsKeyWindow(nint window)
{
var selector = sel_registerName("isKeyWindow");
return SendBool(window, sel_registerName("respondsToSelector:"), selector) && SendBool(window, selector);
}
}
Loading
Loading