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
4 changes: 4 additions & 0 deletions src/EasyWindowsApplication/Core/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ private void RegisterAlternative(WindowModel window)
var surface = new Menus.MenuSurface(window.Content as ContentModel);
if (!string.IsNullOrEmpty(window.Name))
surface.Name = window.Name;
// Dueño de respaldo sin tray: main HWND resuelto en Show() (lazy,
// igual que diálogos). WireMenuSurfaces() lo sobrescribe con el
// broker del tray si hay SystemTray declarado.
surface.OwnerProvider = () => _mainHwnd;
surface.Registry = _registry;
surface.EnsureMaterialized();
_registry.Attach(EntityKind.Surface, surface, hwnd: 0, window.Name);
Expand Down
14 changes: 12 additions & 2 deletions src/EasyWindowsApplication/Core/MasterRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,18 @@ private void DispatchTypedEvent(uint msg, nint controlHwnd, nint wParam, nint lP
nint code = Win32Helpers.HIWORD(wParam);
if (control is IInputFeed feed)
{
if (code == BN.CLICKED) feed.FeedTrigger(typeof(MainTap), 1);
if (code == BN.DOUBLECLICKED) feed.FeedTrigger(typeof(MainTap), 2);
bool fires = code == BN.CLICKED || code == BN.DOUBLECLICKED;
if (!fires) break;
// Publica el HWND origen para MenuSurface.Show() (ancla al
// control). Se limpia al salir: Show() solo lo ve dentro
// del handler en curso. Issue #53, sin cambiar Action.
InputTriggerContext.Publish(controlHwnd);
try
{
if (code == BN.CLICKED) feed.FeedTrigger(typeof(MainTap), 1);
if (code == BN.DOUBLECLICKED) feed.FeedTrigger(typeof(MainTap), 2);
}
finally { InputTriggerContext.Clear(); }
}
break;
}
Expand Down
51 changes: 49 additions & 2 deletions src/EasyWindowsApplication/Core/Menus/MenuSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ internal sealed class MenuSurface : IMenu

internal HandleRegistry? Registry { get; set; }

// Dueño de respaldo resuelto en Show(): diálogos usan el mismo patrón
// (DialogSurfaceBase.OwnerProvider) para no depender del orden de
// declaración en Layout. El tray lo sobrescribe vía SetOwner().
internal Func<nint> OwnerProvider { get; set; } = static () => 0;

internal void SetOwner(nint hwnd) => _ownerHwnd = hwnd;

internal void SetIconAnchor(nint trayHwnd, uint trayId)
Expand Down Expand Up @@ -55,6 +60,15 @@ internal void SetIconAnchor(nint trayHwnd, uint trayId)

public void Show()
{
// Inteligente (issue #53, sin cambiar IMenu): control disparador en
// curso → icono del tray → cursor. Las explícitas ShowAtCursor() /
// ShowAtIcon() no leen el contexto por contrato.
if (InputTriggerContext.TryGetFresh(out var triggerHwnd)
&& TryAnchorToControl(triggerHwnd, out var pos, out var align, out var owner))
{
ShowAt(pos.X, pos.Y, align, owner);
return;
}
if (_iconAnchor is not null)
{
var (x, y) = _iconAnchor();
Expand Down Expand Up @@ -153,9 +167,42 @@ private MenuItemModel ToItemModel(IViewSurface surface, ContentModel? sub)
return model;
}

private void ShowAt(int x, int y, uint align)
private void ShowAt(int x, int y, uint align) => ShowAt(x, y, align, ResolveOwner());

private void ShowAt(int x, int y, uint align, nint owner)
{
var model = EnsureMaterialized();
Win32MenuEngine.Show(_ownerHwnd, model, x, y, align);
Win32MenuEngine.Show(owner, model, x, y, align);
}

/// <summary>
/// Ancla al rect del control disparador con flip según el área de trabajo
/// del monitor (igual que el ancla del icono del tray). El dueño es el
/// propio control para ese Show() puntual. Falla si el HWND ya no es válido.
/// </summary>
private static bool TryAnchorToControl(nint hwnd, out (int X, int Y) pos, out uint align, out nint owner)
{
pos = default;
align = TPM.LEFTALIGN | TPM.TOPALIGN;
owner = hwnd;
if (!Win32.GetWindowRect(hwnd, out RECT rc)) return false;
nint mon = Win32.MonitorFromWindow(hwnd, MONITOR.DEFAULTTONEAREST);
if (mon == 0) mon = Win32.MonitorFromWindow(hwnd, MONITOR.DEFAULTTOPRIMARY);
if (mon == 0)
{
pos = (rc.Left, rc.Bottom);
return true;
}
MONITORINFO mi = new() { cbSize = (uint)Marshal.SizeOf<MONITORINFO>() };
if (!Win32.GetMonitorInfoW(mon, ref mi)) return false;
var work = mi.rcWork;
bool below = work.Bottom - rc.Bottom >= rc.Top - work.Top;
bool toRight = work.Right - rc.Left >= rc.Right - work.Left;
pos = below ? (rc.Left, rc.Bottom) : (rc.Left, rc.Top);
align = (toRight ? TPM.LEFTALIGN : TPM.RIGHTALIGN)
| (below ? TPM.TOPALIGN : TPM.BOTTOMALIGN);
return true;
}

private nint ResolveOwner() => _ownerHwnd != 0 ? _ownerHwnd : OwnerProvider();
}
29 changes: 29 additions & 0 deletions src/EasyWindowsApplication/Core/Surfaces/InputTriggerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace EasyWindowsApplication.Core.Surfaces;

/// <summary>
/// HWND del control que disparó el trigger en curso (hilo UI).
/// Permite a <c>MenuSurface.Show()</c> anclarse al control sin cambiar
/// las firmas <c>Action</c> de <c>IInputSurface</c>. Solo vive durante
/// el dispatch síncrono (<c>Fire</c> → handlers); se limpia con
/// <c>try/finally</c> en cada punto de publicación.
/// </summary>
internal static class InputTriggerContext
{
private static readonly AsyncLocal<(nint Hwnd, int Tick)> _current = new();

internal static void Publish(nint hwnd)
{
if (hwnd == 0) return;
_current.Value = (hwnd, Environment.TickCount);
}

internal static void Clear() => _current.Value = default;

internal static bool TryGetFresh(out nint hwnd, int maxAgeMs = 2000)
{
var (h, tick) = _current.Value;
hwnd = h;
if (h == 0) return false;
return unchecked(Environment.TickCount - tick) <= maxAgeMs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ private void OnHover()
private void Fire(Type trigger)
{
int count = _counter.Next(trigger, Environment.TickCount, Win32.GetDoubleClickTime());
_dispatch(trigger, count);
// El tray no es un control anclable: limpia cualquier contexto previo
// para que MenuSurface.Show() caiga al icono (iconAnchor), nunca a un
// botón de una ventana. Issue #53.
InputTriggerContext.Clear();
try { _dispatch(trigger, count); }
finally { InputTriggerContext.Clear(); }
}

private NOTIFYICONDATAW NewData(uint flags)
Expand Down
12 changes: 0 additions & 12 deletions src/EasyWindowsApplication/Share/CaptionButtons.cs

This file was deleted.

39 changes: 38 additions & 1 deletion src/EasyWindowsApplication/Share/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,41 @@ public enum GridUnitType { Absolute, Auto, Star }

public enum LayoutAlignment { Start, Center, End, Fill }

public enum DockPosition { Left, Top, Right, Bottom }
public enum DockPosition { Left, Top, Right, Bottom }

/// <summary>Botones de caption nativos.</summary>
[Flags]
public enum CaptionButtons { None = 0, Close = 1, Minimize = 2, Maximize = 4, All = Close | Minimize | Maximize }

/// <summary>Estado de la ventana principal.</summary>
public enum WindowState { Normal, Minimized, Maximized, FullScreen }

public enum WindowPositionPreset { Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight }

public enum SnapRegion { Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight }

/// <summary>Fondo translúcido del shell (DWM). Requiere Windows 10 1809+ para efectos; en sistemas anteriores se ignora.</summary>
public enum WindowBackdrop { None, Mica, Acrylic, DesktopAcrylic, SolidColor }

/// <summary>Modo de title bar de la ventana principal.</summary>
public enum TitleBarMode
{
/// <summary>Title bar nativa Win32 (WNDCLASSEX icon + SetWindowText).</summary>
Simple,

/// <summary>Custom title bar extendida sobre el client area (DwmExtendFrameIntoClientArea).</summary>
Custom
}

/// <summary>Modo de visibilidad en capturas de pantalla (PrtSc, Teams, etc.).</summary>
public enum ScreenCaptureMode
{
/// <summary>Contenido normal, capturable.</summary>
Allow,

/// <summary>La ventana aparece negra en captura (WDA_EXCLUDEFROMCAPTURE, Win10 2004+).</summary>
HideContent,

/// <summary>La ventana no aparece en captura (WDA_EXCLUDEFROMCAPTURE con fallback a WDA_MONITOR).</summary>
HideWindow
}
6 changes: 1 addition & 5 deletions src/EasyWindowsApplication/Share/ILayout.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace EasyWindowsApplication.Share;
namespace EasyWindowsApplication.Share;

public interface ILayout
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
namespace EasyWindowsApplication.Share;

/// <summary>Modo de title bar de la ventana principal.</summary>
public enum TitleBarMode
{
/// <summary>Title bar nativa Win32 (WNDCLASSEX icon + SetWindowText).</summary>
Simple,

/// <summary>Custom title bar extendida sobre el client area (DwmExtendFrameIntoClientArea).</summary>
Custom,
}

/// <summary>Configuración de title bar.</summary>
public interface ITitleBarConfig
{
Expand Down
14 changes: 0 additions & 14 deletions src/EasyWindowsApplication/Share/ScreenCaptureMode.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/EasyWindowsApplication/Share/View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal ViewBase() { }

public sealed class View<T> where T : class, IViewSurface
{
private readonly List<Action<T>> _configure = new();
private readonly List<Action<T>> _configure = [];
internal string? PendingName { get; private set; }

public View() { }
Expand Down
4 changes: 0 additions & 4 deletions src/EasyWindowsApplication/Share/WindowBackdrop.cs

This file was deleted.

25 changes: 0 additions & 25 deletions src/EasyWindowsApplication/Share/WindowPosition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,3 @@ public sealed record PositionPreset(WindowPositionPreset Preset) : IWindowPositi

/// <summary>Snap a borde de pantalla (o trackbar) estilo Aero Snap.</summary>
public sealed record PositionSnap(SnapRegion Region) : IWindowPosition;

public enum WindowPositionPreset
{
Center,
TopLeft,
Top,
TopRight,
Left,
Right,
BottomLeft,
Bottom,
BottomRight,
}

public enum SnapRegion
{
Left,
Right,
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
4 changes: 0 additions & 4 deletions src/EasyWindowsApplication/Share/WindowState.cs

This file was deleted.

Loading
Loading