From f3a8867a6bb46f831ea51028eb8ba2622c8ec06a Mon Sep 17 00:00:00 2001 From: Raul Avila Catala Date: Thu, 17 Sep 2026 07:58:19 -0600 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20motor=20de=20men=C3=BAs=20declara?= =?UTF-8?q?tivo=20+=20SystemTray=20con=20triggers=20tipados=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fase 0: IViewSurface como contrato común (View/IControl/ViewModel relajados), IMenu/IMenuItem. Fase 1: motor HMENU Win32 puro (Win32MenuEngine + MenuSurface + MenuItem). Fase 2: SystemTray (broker + triggers tipados sin nombres de dispositivo, LongTap/Holding derivados), AlternativeWindow, rama IMenu, .SystemTray() en IWindowConfig. Fase 3: generator (GetSurface), bh.SystemTray, fusión IAppBehavior (TaskbarButtonVisibility, OnLaunched real). Fase 4: docs (system_design + propuestas) y Sample compilable. Extra: IMenuItemSeparator + overload View() anónimo. Breaking: eliminados ISystemTrayConfig/ISystemTrayMenuBuilder, ShowInTaskbar/SetupSystemTray/RemoveSystemTray. --- .../EasyBehaviorGenerator.cs | 113 ++++++-- .../Core/AppBehaviorBuilderImpl.cs | 62 +++++ .../Core/Application.cs | 45 ++++ .../Core/BehaviorBuilderImpl.cs | 16 +- src/EasyWindowsApplication/Core/Constants.cs | 71 +++++ .../Core/HandleRegistry.cs | 13 + .../Core/LayoutEngine/ChildrenBuilderImpl.cs | 17 +- .../Core/LayoutEngine/LayoutBuilderImpl.cs | 9 + .../Core/LayoutEngine/WindowConfigBuilder.cs | 6 + .../Core/LayoutModels.cs | 7 +- .../Core/MasterRouter.cs | 6 +- .../Core/Menus/MenuItem.cs | 28 ++ .../Core/Menus/MenuModel.cs | 38 +++ .../Core/Menus/MenuSeparatorItem.cs | 25 ++ .../Core/Menus/MenuSurface.cs | 165 ++++++++++++ .../Core/Menus/Win32MenuEngine.cs | 89 +++++++ .../Core/ResourcesDictionaryImpl.cs | 6 + .../Core/ShellEntities.cs | 32 +++ .../Core/SystemTray/SystemTrayBroker.cs | 252 ++++++++++++++++++ .../Core/SystemTray/SystemTrayImpl.cs | 114 ++++++++ .../Core/TaskbarList.cs | 57 ++++ src/EasyWindowsApplication/Core/Win32.cs | 56 ++++ .../Core/Windowing/AlternativeWindowImpl.cs | 13 +- .../Core/Windowing/Procedures.cs | 63 +++++ .../Core/Windowing/Win32.cs | 3 + .../Core/Windowing/WindowImpl.cs | 13 +- .../Share/IAlternativeWindow.cs | 1 + .../Share/IAppBehavior.cs | 11 + .../Share/IBehaviorBuilder.cs | 6 +- .../Share/IChildrenBuilder.cs | 9 +- .../Share/ILayoutBuilder.cs | 3 + .../Share/ILayoutBuilderAfterWindow.cs | 1 + src/EasyWindowsApplication/Share/IMenu.cs | 10 + src/EasyWindowsApplication/Share/IMenuItem.cs | 9 + .../Share/IMenuItemSeparator.cs | 9 + .../Share/IResourcesDictionary.cs | 5 +- .../Share/ISystemTray.cs | 17 ++ .../Share/IViewSurface.cs | 31 +++ src/EasyWindowsApplication/Share/IWindow.cs | 1 + .../Share/IWindowConfig.cs | 1 + .../Share/Infrastructure/ControlAccess.cs | 8 + .../Share/Input/ISpatialPositionTrigger.cs | 17 ++ .../Share/Input/KeyPrimitives.cs | 43 +++ .../Share/Input/TapCount.cs | 23 ++ .../Input/WithoutSpatialPositionTrigger.cs | 11 + src/EasyWindowsApplication/Share/View.cs | 8 +- .../Win32ControlsModule/Frontend/IControl.cs | 18 +- src/Sample/Program.cs | 82 ++++-- src/Sample/Sample.csproj | 2 +- src/system_design.md | 88 +++++- 50 files changed, 1629 insertions(+), 104 deletions(-) create mode 100644 src/EasyWindowsApplication/Core/AppBehaviorBuilderImpl.cs create mode 100644 src/EasyWindowsApplication/Core/Menus/MenuItem.cs create mode 100644 src/EasyWindowsApplication/Core/Menus/MenuModel.cs create mode 100644 src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs create mode 100644 src/EasyWindowsApplication/Core/Menus/MenuSurface.cs create mode 100644 src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs create mode 100644 src/EasyWindowsApplication/Core/ShellEntities.cs create mode 100644 src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs create mode 100644 src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs create mode 100644 src/EasyWindowsApplication/Core/TaskbarList.cs create mode 100644 src/EasyWindowsApplication/Share/IAppBehavior.cs create mode 100644 src/EasyWindowsApplication/Share/IMenu.cs create mode 100644 src/EasyWindowsApplication/Share/IMenuItem.cs create mode 100644 src/EasyWindowsApplication/Share/IMenuItemSeparator.cs create mode 100644 src/EasyWindowsApplication/Share/ISystemTray.cs create mode 100644 src/EasyWindowsApplication/Share/IViewSurface.cs create mode 100644 src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs create mode 100644 src/EasyWindowsApplication/Share/Input/KeyPrimitives.cs create mode 100644 src/EasyWindowsApplication/Share/Input/TapCount.cs create mode 100644 src/EasyWindowsApplication/Share/Input/WithoutSpatialPositionTrigger.cs diff --git a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs index c609258..a371643 100644 --- a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs +++ b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs @@ -107,18 +107,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { if (!usesWinApi && !item.IsWindowType) continue; var propName = SanitizeName(item.Name); - if (item.IsWindowType) - { - sb.AppendLine($" public {item.TypeSymbol} {propName}"); - sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); - sb.AppendLine($" .GetWindow<{item.TypeSymbol}>(\"{item.Name}\");"); - } - else - { - sb.AppendLine($" public {item.TypeSymbol} {propName}"); - sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); - sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");"); - } + var lookup = item.IsWindowType + ? $"GetWindow<{item.TypeSymbol}>(\"{item.Name}\")" + : item.IsSurfaceType + ? $"GetSurface<{item.TypeSymbol}>(\"{item.Name}\")" + : $"Get<{item.TypeSymbol}>(\"{item.Name}\")"; + sb.AppendLine($" public {item.TypeSymbol} {propName}"); + sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); + sb.AppendLine($" .{lookup};"); sb.AppendLine(); } @@ -147,18 +143,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { if (!usesWinApi && !item.IsWindowType) continue; var propName = SanitizeName(item.Name); - if (item.IsWindowType) - { - sb.AppendLine($" internal static {item.TypeSymbol} {propName}"); - sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); - sb.AppendLine($" .GetWindow<{item.TypeSymbol}>(\"{item.Name}\");"); - } - else - { - sb.AppendLine($" internal static {item.TypeSymbol} {propName}"); - sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); - sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");"); - } + var lookup = item.IsWindowType + ? $"GetWindow<{item.TypeSymbol}>(\"{item.Name}\")" + : item.IsSurfaceType + ? $"GetSurface<{item.TypeSymbol}>(\"{item.Name}\")" + : $"Get<{item.TypeSymbol}>(\"{item.Name}\")"; + sb.AppendLine($" internal static {item.TypeSymbol} {propName}"); + sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess"); + sb.AppendLine($" .{lookup};"); sb.AppendLine(); } @@ -228,22 +220,84 @@ private static bool IsNameCandidate(SyntaxNode node) name, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), isWindowType: false, + isSurfaceType: IsSurfaceType(context, typeSymbol), location); } if (parentMa.Name.Identifier.Text == "Window") { - return new NamedInfo(name, "global::EasyWindowsApplication.Share.IWindow", isWindowType: true, location); + return new NamedInfo(name, "global::EasyWindowsApplication.Share.IWindow", isWindowType: true, isSurfaceType: false, location); + } + + if (parentMa.Name is GenericNameSyntax altGeneric + && altGeneric.Identifier.Text == "AlternativeWindow" + && altGeneric.TypeArgumentList.Arguments.Count == 1) + { + var typeArg = altGeneric.TypeArgumentList.Arguments[0]; + var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg).Type; + if (typeSymbol == null) return null; + + var compilation = context.SemanticModel.Compilation; + var iBaseWindow = compilation.GetTypeByMetadataName( + "EasyWindowsApplication.Share.IBaseWindow"); + if (iBaseWindow is not null && Implements(typeSymbol, iBaseWindow)) + { + return new NamedInfo( + name, + typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + isWindowType: true, + isSurfaceType: false, + location); + } + if (IsSurfaceType(context, typeSymbol)) + { + return new NamedInfo( + name, + typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + isWindowType: false, + isSurfaceType: true, + location); + } + return null; } if (parentMa.Name.Identifier.Text == "AlternativeWindow") { - return new NamedInfo(name, "global::EasyWindowsApplication.Share.IAlternativeWindow", isWindowType: true, location); + return new NamedInfo(name, "global::EasyWindowsApplication.Share.IAlternativeWindow", isWindowType: true, isSurfaceType: false, location); } return null; } + private static bool IsSurfaceType(GeneratorSyntaxContext context, ITypeSymbol typeSymbol) + { + var iControl = context.SemanticModel.Compilation.GetTypeByMetadataName( + "EasyWindowsApplication.Win32ControlsModule.Frontend.IControl"); + if (iControl is not null && Implements(typeSymbol, iControl)) + return false; + + var iViewSurface = context.SemanticModel.Compilation.GetTypeByMetadataName( + "EasyWindowsApplication.Share.IViewSurface"); + if (iViewSurface is null) + return false; + + return Implements(typeSymbol, iViewSurface); + } + + private static bool Implements(ITypeSymbol typeSymbol, ITypeSymbol iface) + { + if (typeSymbol.Equals(iface, SymbolEqualityComparer.Default)) + return true; + + foreach (var implemented in typeSymbol.AllInterfaces) + { + if (implemented.Equals(iface, SymbolEqualityComparer.Default)) + return true; + } + + return false; + } + private static bool IsBehaviorCandidate(SyntaxNode node) { if (node is not InvocationExpressionSyntax inv) @@ -386,18 +440,19 @@ private static string SanitizeName(string name) return sb.ToString(); } - private readonly struct NamedInfo(string name, string typeSymbol, bool isWindowType, Location location) + private readonly struct NamedInfo(string name, string typeSymbol, bool isWindowType, bool isSurfaceType, Location location) { public readonly string Name = name; public readonly string TypeSymbol = typeSymbol; public readonly bool IsWindowType = isWindowType; + public readonly bool IsSurfaceType = isSurfaceType; public readonly Location Location = location; public override bool Equals(object obj) => - obj is NamedInfo other && Name == other.Name && TypeSymbol == other.TypeSymbol && IsWindowType == other.IsWindowType; + obj is NamedInfo other && Name == other.Name && TypeSymbol == other.TypeSymbol && IsWindowType == other.IsWindowType && IsSurfaceType == other.IsSurfaceType; public override int GetHashCode() => - (Name?.GetHashCode() ?? 0) ^ (TypeSymbol?.GetHashCode() ?? 0) ^ IsWindowType.GetHashCode(); + (Name?.GetHashCode() ?? 0) ^ (TypeSymbol?.GetHashCode() ?? 0) ^ IsWindowType.GetHashCode() ^ IsSurfaceType.GetHashCode(); } private sealed class EnclosingTypeInfo(string ns, string className) diff --git a/src/EasyWindowsApplication/Core/AppBehaviorBuilderImpl.cs b/src/EasyWindowsApplication/Core/AppBehaviorBuilderImpl.cs new file mode 100644 index 0000000..055b191 --- /dev/null +++ b/src/EasyWindowsApplication/Core/AppBehaviorBuilderImpl.cs @@ -0,0 +1,62 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core; + +internal sealed class AppBehaviorBuilderImpl : IAppBehavior +{ + private readonly List _launched = new(); + private readonly List> _launchedWithSelf = new(); + private readonly List> _terminating = new(); + private readonly List _terminated = new(); + + internal nint MainHwnd { get; set; } + + public IAppBehavior OnLaunched(Action handler) + { + _launched.Add(handler); + return this; + } + + public IAppBehavior OnLaunched(Action handler) + { + _launchedWithSelf.Add(handler); + return this; + } + + public IAppBehavior OnTerminating(Action handler) + { + _terminating.Add(handler); + return this; + } + + public IAppBehavior OnTerminated(Action handler) + { + _terminated.Add(handler); + return this; + } + + public IAppBehavior TaskbarButtonVisibility(bool visible) + { + if (MainHwnd != 0) + TaskbarList.SetVisible(MainHwnd, visible); + return this; + } + + internal void RaiseLaunched() + { + foreach (var handler in _launched.ToArray()) handler(); + foreach (var handler in _launchedWithSelf.ToArray()) handler(this); + } + + internal bool RaiseTerminating() + { + var args = new CancelEventArgs(); + foreach (var handler in _terminating.ToArray()) handler(args); + return args.Cancel; + } + + internal void RaiseTerminated() + { + foreach (var handler in _terminated.ToArray()) handler(); + } +} diff --git a/src/EasyWindowsApplication/Core/Application.cs b/src/EasyWindowsApplication/Core/Application.cs index c1b4079..e33e9a6 100644 --- a/src/EasyWindowsApplication/Core/Application.cs +++ b/src/EasyWindowsApplication/Core/Application.cs @@ -19,6 +19,9 @@ internal sealed class Application : private MasterRouter _router = null!; private readonly HandleRegistry _registry = new(); private Action? _pendingBehavior; + private nint _mainHwnd; + private SystemTray.SystemTrayBroker? _trayBroker; + private readonly List _pendingMenuSurfaces = new(); public IApplicationLayoutPhase Resources(Action configure) { @@ -81,8 +84,10 @@ public void Initialize() if (_pendingBehavior is not null) { BehaviorBuilder.Registry = _registry; + BehaviorBuilder.MainHwnd = _mainHwnd; ControlAccess.SetController(BehaviorBuilder); _pendingBehavior(BehaviorBuilder); + BehaviorBuilder.RaiseLaunched(); } Procedures.RunMessageLoop(); @@ -91,6 +96,7 @@ public void Initialize() private void RegisterMain(WindowModel window) { var hwnd = Core.Windowing.Procedures.CreateMainWindow(_router, window.Title, window.Width, window.Height); + _mainHwnd = hwnd; var win = new Core.Windowing.WindowImpl( hwnd, window.Name, window.Title, window.Width, window.Height, window.Position); @@ -109,10 +115,49 @@ private void RegisterMain(WindowModel window) win.Center(); win.Show(); win.RaiseLoaded(); + if (window.SystemTrayConfigure is { } trayConfigure) + SetupSystemTrayIcon(trayConfigure, hwnd); + } + + private void SetupSystemTrayIcon(Action configure, nint mainHwnd) + { + SystemTray.SystemTrayImpl? tray = null; + var broker = SystemTray.SystemTrayBroker.Create(_router, (trigger, count) => tray?.DispatchTrigger(trigger, count)); + tray = new SystemTray.SystemTrayImpl(broker); + configure(tray); + nint hIcon = Core.Windowing.Procedures.LoadAppIcon(); + broker.AddIcon(hIcon); + _registry.RegisterSurface(tray.Name, tray); + _trayBroker = broker; + WireMenuSurfaces(); + _router.RegisterHandler(mainHwnd, WM.DESTROY, (w, l) => { broker.Shutdown(); return 0; }); + } + + private void WireMenuSurfaces() + { + if (_trayBroker is null) return; + foreach (var surface in _pendingMenuSurfaces) + { + surface.SetOwner(_trayBroker.Hwnd); + surface.SetIconAnchor(_trayBroker.Hwnd, SystemTray.SystemTrayBroker.IconId); + } + _pendingMenuSurfaces.Clear(); } private void RegisterAlternative(WindowModel window) { + if (window.SurfaceType is not null && typeof(IMenu).IsAssignableFrom(window.SurfaceType)) + { + var surface = new Menus.MenuSurface(window.Content as ContentModel); + if (!string.IsNullOrEmpty(window.Name)) + surface.Name = window.Name; + surface.Registry = _registry; + surface.EnsureMaterialized(); + _registry.RegisterSurface(window.Name, surface); + _pendingMenuSurfaces.Add(surface); + WireMenuSurfaces(); + return; + } var hwnd = Core.Windowing.Procedures.CreateAlternativeWindow(_router, 0, window.Title, window.Width, window.Height); var win = new Core.Windowing.AlternativeWindowImpl(hwnd, 0, window.Name, window.Title, window.Width, window.Height, window.Position); if (window.Background.HasValue) diff --git a/src/EasyWindowsApplication/Core/BehaviorBuilderImpl.cs b/src/EasyWindowsApplication/Core/BehaviorBuilderImpl.cs index 5d2852b..ea23c53 100644 --- a/src/EasyWindowsApplication/Core/BehaviorBuilderImpl.cs +++ b/src/EasyWindowsApplication/Core/BehaviorBuilderImpl.cs @@ -3,13 +3,27 @@ namespace EasyWindowsApplication.Core; -internal sealed class BehaviorBuilderImpl : IBehaviorBuilder, ControlAccess.IBehaviorServicesController +internal sealed class BehaviorBuilderImpl : IBehaviorBuilder, ControlAccess.IBehaviorServicesController, ControlAccess.ISurfaceServicesController { + private AppBehaviorBuilderImpl? _appBehavior; + private ISystemTray? _systemTray; + + public IAppBehavior WindowsApplication => _appBehavior ??= new AppBehaviorBuilderImpl() { MainHwnd = MainHwnd }; + + public ISystemTray SystemTray => _systemTray ??= (ISystemTray)Registry!.GetSurface("SystemTray")!; + internal HandleRegistry? Registry { get; set; } + internal nint MainHwnd { get; set; } + + internal void RaiseLaunched() => _appBehavior?.RaiseLaunched(); + T ControlAccess.IBehaviorServicesController.Get(string name) => (T)Registry!.GetByName(name)!; T ControlAccess.IBehaviorServicesController.GetWindow(string name) => (T)Registry!.GetWindow(name)!; + + T ControlAccess.ISurfaceServicesController.GetSurface(string name) + => (T)Registry!.GetSurface(name)!; } diff --git a/src/EasyWindowsApplication/Core/Constants.cs b/src/EasyWindowsApplication/Core/Constants.cs index bc1b04e..7019af8 100644 --- a/src/EasyWindowsApplication/Core/Constants.cs +++ b/src/EasyWindowsApplication/Core/Constants.cs @@ -94,6 +94,7 @@ internal static class WM internal const uint EXITSIZEMOVE = 0x0232; internal const uint SETTINGCHANGE = 0x001A; internal const uint DPICHANGED = 0x02E0; + internal const uint CONTEXTMENU = 0x007B; } internal static class SPI @@ -225,3 +226,73 @@ internal static class SWP internal const uint NOOWNERZORDER = 0x0200; internal const uint NOSENDCHANGING = 0x0400; } + + internal static class MF + { + internal const uint STRING = 0x00000000; + internal const uint SEPARATOR = 0x00000800; + internal const uint POPUP = 0x00000010; + internal const uint GRAYED = 0x00000001; + internal const uint DISABLED = 0x00000002; + internal const uint CHECKED = 0x00000008; + internal const uint UNCHECKED = 0x00000000; + internal const uint BYCOMMAND = 0x00000000; + internal const uint BYPOSITION = 0x00000400; + } + + internal static class TPM + { + internal const uint LEFTALIGN = 0x0000; + internal const uint CENTERALIGN = 0x0004; + internal const uint RIGHTALIGN = 0x0008; + internal const uint TOPALIGN = 0x0000; + internal const uint VCENTERALIGN = 0x0010; + internal const uint BOTTOMALIGN = 0x0020; + internal const uint NONOTIFY = 0x0080; + internal const uint RETURNCMD = 0x0100; + } + + internal static class NIM + { + internal const uint ADD = 0x00000000; + internal const uint MODIFY = 0x00000001; + internal const uint DELETE = 0x00000002; + internal const uint SETFOCUS = 0x00000003; + internal const uint SETVERSION = 0x00000004; + } + + internal static class NIF + { + internal const uint MESSAGE = 0x00000001; + internal const uint ICON = 0x00000002; + internal const uint TIP = 0x00000004; + internal const uint STATE = 0x00000008; + internal const uint INFO = 0x00000010; + internal const uint GUID = 0x00000020; + internal const uint SHOWTIP = 0x00000080; + } + + internal static class NIN + { + internal const uint SELECT = 0x0400; + internal const uint KEYSELECT = 0x0401; + internal const uint BALLOONSHOW = 0x0402; + internal const uint BALLOONHIDE = 0x0403; + internal const uint BALLOONTIMEOUT = 0x0404; + internal const uint BALLOONUSERCLICK = 0x0405; + internal const uint POPUPOPEN = 0x0406; + internal const uint POPUPCLOSE = 0x0407; + } + + internal static class NOTIFYICON + { + internal const uint VERSION_4 = 4; + } + + internal static class VK + { + internal const int SHIFT = 0x10; + internal const int CONTROL = 0x11; + internal const int MENU = 0x12; + internal const int APPS = 0x5D; + } diff --git a/src/EasyWindowsApplication/Core/HandleRegistry.cs b/src/EasyWindowsApplication/Core/HandleRegistry.cs index a684009..a85bcc5 100644 --- a/src/EasyWindowsApplication/Core/HandleRegistry.cs +++ b/src/EasyWindowsApplication/Core/HandleRegistry.cs @@ -15,6 +15,7 @@ internal sealed class HandleRegistry private readonly Dictionary> _hwndToControl = new(); private readonly Dictionary> _nameToControl = new(StringComparer.Ordinal); private readonly Dictionary _windowByName = new(StringComparer.Ordinal); + private readonly Dictionary _surfaceByName = new(StringComparer.Ordinal); private readonly Dictionary _hwndToWindow = new(); private readonly Dictionary> _windowToControls = new(); @@ -146,6 +147,18 @@ internal void UnregisterWindowByHwnd(nint hwnd) internal IBaseWindow? GetWindow(string name) => _windowByName.TryGetValue(name, out var w) ? w : null; + internal void RegisterSurface(string name, IViewSurface surface) + { + if (!string.IsNullOrEmpty(name)) + _surfaceByName[name] = surface; + } + + internal IViewSurface? GetSurface(string name) + => _surfaceByName.TryGetValue(name, out var s) ? s : null; + + internal T? GetSurface(string name) where T : class, IViewSurface + => GetSurface(name) as T; + internal IEnumerable AllControlHandles() { foreach (var kv in _hwndToControl) diff --git a/src/EasyWindowsApplication/Core/LayoutEngine/ChildrenBuilderImpl.cs b/src/EasyWindowsApplication/Core/LayoutEngine/ChildrenBuilderImpl.cs index f39cc3c..5b073e2 100644 --- a/src/EasyWindowsApplication/Core/LayoutEngine/ChildrenBuilderImpl.cs +++ b/src/EasyWindowsApplication/Core/LayoutEngine/ChildrenBuilderImpl.cs @@ -1,7 +1,6 @@ using EasyWindowsApplication.Common; using EasyWindowsApplication.Core; using EasyWindowsApplication.Share; -using EasyWindowsApplication.Win32ControlsModule.Frontend; namespace EasyWindowsApplication.Core.LayoutEngine; @@ -11,7 +10,19 @@ internal sealed class ChildrenBuilderImpl : IChildrenBuilder internal ChildrenBuilderImpl(ContentModel content) => _content = content; - public IChildrenBuilder View(Action> configure) where T : class, IControl + public IChildrenBuilder View() where T : class, IViewSurface + { + var view = new View(); + _content.Children.Add(new ViewModel + { + Name = view.PendingName ?? "", + ControlType = typeof(T), + Configure = view.BuildConfigure() + }); + return this; + } + + public IChildrenBuilder View(Action> configure) where T : class, IViewSurface { var view = new View(); configure(view); @@ -25,7 +36,7 @@ public IChildrenBuilder View(Action> configure) where T : class, ICon return this; } - public IChildrenBuilder View(Func, View> configure) where T : class, IControl + public IChildrenBuilder View(Func, View> configure) where T : class, IViewSurface { var view = new View(); var result = configure(view); diff --git a/src/EasyWindowsApplication/Core/LayoutEngine/LayoutBuilderImpl.cs b/src/EasyWindowsApplication/Core/LayoutEngine/LayoutBuilderImpl.cs index e7556e4..6f3cc3b 100644 --- a/src/EasyWindowsApplication/Core/LayoutEngine/LayoutBuilderImpl.cs +++ b/src/EasyWindowsApplication/Core/LayoutEngine/LayoutBuilderImpl.cs @@ -38,4 +38,13 @@ public ILayoutBuilderAfterWindow AlternativeWindow(Action configu _app.AddWindow(model); return this; } + + public ILayoutBuilderAfterWindow AlternativeWindow(Action configure) where T : class, IViewSurface + { + var model = new WindowModel { IsAlternative = true, SurfaceType = typeof(T) }; + var config = new WindowConfigBuilder(model); + configure(config); + _app.AddWindow(model); + return this; + } } diff --git a/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs b/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs index b56594d..62c1329 100644 --- a/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs +++ b/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs @@ -48,6 +48,12 @@ public IWindowConfig Scroll(Action configure) return this; } + public IWindowConfig SystemTray(Action configure) + { + _model.SystemTrayConfigure = configure; + return this; + } + public IWindowContentConfig Content(Action configure) where TLayout : IStackLayout => RegisterContent(configure); diff --git a/src/EasyWindowsApplication/Core/LayoutModels.cs b/src/EasyWindowsApplication/Core/LayoutModels.cs index 035ce8b..e1fda75 100644 --- a/src/EasyWindowsApplication/Core/LayoutModels.cs +++ b/src/EasyWindowsApplication/Core/LayoutModels.cs @@ -1,5 +1,4 @@ using EasyWindowsApplication.Share; -using EasyWindowsApplication.Win32ControlsModule.Frontend; namespace EasyWindowsApplication.Core; @@ -17,6 +16,8 @@ internal sealed class WindowModel internal bool IsAlternative { get; set; } internal IContentModel? Content { get; set; } internal WindowsScroll? ScrollConfig { get; set; } + internal Action? SystemTrayConfigure { get; set; } + internal Type? SurfaceType { get; set; } } internal sealed class ContentModel : IContentModel @@ -29,8 +30,8 @@ internal sealed class ContentModel : IContentModel internal sealed class ViewModel : IViewModel { internal string Name { get; set; } = ""; - internal IControl? Control { get; set; } + internal IViewSurface? Control { get; set; } internal ContentModel? SubContent { get; set; } internal Type? ControlType { get; set; } - internal Action? Configure { get; set; } + internal Action? Configure { get; set; } } diff --git a/src/EasyWindowsApplication/Core/MasterRouter.cs b/src/EasyWindowsApplication/Core/MasterRouter.cs index 8b1c78c..9ff73dc 100644 --- a/src/EasyWindowsApplication/Core/MasterRouter.cs +++ b/src/EasyWindowsApplication/Core/MasterRouter.cs @@ -305,10 +305,10 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) return ctrlBrush; } - // 1. Try registered raw handlers (lowest level) - if (hwnd == _mainHwnd && _handlers.TryGetValue((hwnd, msg), out var mainWindowHandler)) + // 1. Try registered raw handlers (lowest level, any window incl. hidden broker windows) + if (_handlers.TryGetValue((hwnd, msg), out var windowHandler)) { - var result = mainWindowHandler(wParam, lParam); + var result = windowHandler(wParam, lParam); if (result != 0) return result; } diff --git a/src/EasyWindowsApplication/Core/Menus/MenuItem.cs b/src/EasyWindowsApplication/Core/Menus/MenuItem.cs new file mode 100644 index 0000000..a0c517f --- /dev/null +++ b/src/EasyWindowsApplication/Core/Menus/MenuItem.cs @@ -0,0 +1,28 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core.Menus; + +internal sealed class MenuItem : IMenuItem +{ + public string Name { get; set; } = ""; + public string Text { get; set; } = ""; + public bool IsEnabled { get; set; } = true; + public bool IsChecked { get; set; } + public LayoutLength? LayoutWidth { get; set; } + public LayoutLength? LayoutHeight { get; set; } + public LayoutOptions LayoutOptions { get; set; } = new(); + public Thickness Margin { get; set; } + public Thickness Padding { get; set; } + public DockPosition Dock { get; set; } + public int GridRow { get; set; } + public int GridColumn { get; set; } + public int GridRowSpan { get; set; } + public int GridColumnSpan { get; set; } + public Color? BackgroundColor { get; set; } + + public event Action? Clicked; + + public void OnClick(Action handler) => Clicked += handler; + + internal void RaiseClicked() => Clicked?.Invoke(); +} diff --git a/src/EasyWindowsApplication/Core/Menus/MenuModel.cs b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs new file mode 100644 index 0000000..8d6c7ed --- /dev/null +++ b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs @@ -0,0 +1,38 @@ +namespace EasyWindowsApplication.Core.Menus; + +internal sealed class MenuItemModel +{ + internal int Id; + internal string Text = ""; + internal bool IsEnabled = true; + internal bool IsChecked; + internal bool IsSeparator; + internal List Children = new(); + internal Action? OnClick; +} + +internal sealed class MenuModel +{ + internal List Items = new(); + + internal MenuItemModel? FindById(int id) + { + foreach (var item in Items) + { + var found = FindRecursive(item, id); + if (found is not null) return found; + } + return null; + } + + private static MenuItemModel? FindRecursive(MenuItemModel item, int id) + { + if (item.Id == id) return item; + foreach (var child in item.Children) + { + var found = FindRecursive(child, id); + if (found is not null) return found; + } + return null; + } +} diff --git a/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs b/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs new file mode 100644 index 0000000..bdce76b --- /dev/null +++ b/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs @@ -0,0 +1,25 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core.Menus; + +internal sealed class MenuSeparatorItem : IMenuItemSeparator +{ + public string Name { get; set; } = ""; + public LayoutLength? LayoutWidth { get; set; } + public LayoutLength? LayoutHeight { get; set; } + public LayoutOptions LayoutOptions { get; set; } = new(); + public Thickness Margin { get; set; } + public Thickness Padding { get; set; } + public DockPosition Dock { get; set; } + public int GridRow { get; set; } + public int GridColumn { get; set; } + public int GridRowSpan { get; set; } + public int GridColumnSpan { get; set; } + public Color? BackgroundColor { get; set; } + + public event Action? Clicked; + + public void OnClick(Action handler) => Clicked += handler; + + internal void RaiseClicked() => Clicked?.Invoke(); +} diff --git a/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs new file mode 100644 index 0000000..5700bf0 --- /dev/null +++ b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs @@ -0,0 +1,165 @@ +using System.Runtime.InteropServices; +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core.Menus; + +internal sealed class MenuSurface : IMenu +{ + private readonly ContentModel? _content; + private nint _ownerHwnd; + private Func<(int X, int Y)>? _iconAnchor; + private MenuModel? _cachedModel; + + internal MenuSurface(ContentModel? content) => _content = content; + + internal HandleRegistry? Registry { get; set; } + + internal void SetOwner(nint hwnd) => _ownerHwnd = hwnd; + + internal void SetIconAnchor(nint trayHwnd, uint trayId) + { + _iconAnchor = () => + { + var identifier = new NOTIFYICONIDENTIFIER + { + cbSize = (uint)Marshal.SizeOf(), + hWnd = trayHwnd, + uID = trayId, + guidItem = Guid.Empty + }; + if (Win32.Shell_NotifyIconGetRect(ref identifier, out var rect) != 0) + return (rect.Right, rect.Top); + Win32.GetCursorPos(out var pt); + return (pt.X, pt.Y); + }; + } + + public string Name { get; set; } = ""; + public LayoutLength? LayoutWidth { get; set; } + public LayoutLength? LayoutHeight { get; set; } + public LayoutOptions LayoutOptions { get; set; } = new(); + public Thickness Margin { get; set; } + public Thickness Padding { get; set; } + public DockPosition Dock { get; set; } + public int GridRow { get; set; } + public int GridColumn { get; set; } + public int GridRowSpan { get; set; } + public int GridColumnSpan { get; set; } + public Color? BackgroundColor { get; set; } + + public event Action? Clicked; + + public void OnClick(Action handler) => Clicked += handler; + + public void Show() + { + if (_iconAnchor is not null) + { + var (x, y) = _iconAnchor(); + ShowAt(x, y, TPM.RIGHTALIGN | TPM.BOTTOMALIGN); + } + else + { + ShowAtCursor(); + } + } + + public void ShowAtCursor() + { + Win32.GetCursorPos(out var pt); + ShowAt(pt.X, pt.Y, TPM.LEFTALIGN | TPM.TOPALIGN); + } + + public void ShowAtIcon() + { + if (_iconAnchor is not null) + { + var (x, y) = _iconAnchor(); + ShowAt(x, y, TPM.RIGHTALIGN | TPM.BOTTOMALIGN); + } + else + { + ShowAtCursor(); + } + } + + public void Hide() { } + + public void Close() { } + + internal MenuModel BuildModel() + { + var model = new MenuModel(); + if (_content is null) return model; + foreach (var child in _content.Children) + { + if (child is not ViewModel vm) continue; + var surface = CreateSurface(vm); + model.Items.Add(ToItemModel(surface, vm.SubContent)); + } + return model; + } + + internal MenuModel EnsureMaterialized() + { + if (_cachedModel is null) + _cachedModel = BuildModel(); + return _cachedModel; + } + + private static IViewSurface CreateSurface(ViewModel vm) + { + if (vm.Control is IViewSurface existing) return existing; + IViewSurface surface = vm.ControlType is not null + && typeof(IMenuItemSeparator).IsAssignableFrom(vm.ControlType) + ? new MenuSeparatorItem() + : new MenuItem(); + if (!string.IsNullOrEmpty(vm.Name)) + surface.Name = vm.Name; + vm.Configure?.Invoke(surface); + return surface; + } + + private MenuItemModel ToItemModel(IViewSurface surface, ContentModel? sub) + { + if (Registry is not null && !string.IsNullOrEmpty(surface.Name)) + Registry.RegisterSurface(surface.Name, surface); + if (surface is IMenuItemSeparator) + { + return new MenuItemModel + { + Id = Win32MenuEngine.NextId(), + IsSeparator = true + }; + } + var item = (IMenuItem)surface; + var model = new MenuItemModel + { + Id = Win32MenuEngine.NextId(), + Text = item.Text, + IsEnabled = item.IsEnabled, + IsChecked = item.IsChecked, + OnClick = () => + { + if (item is MenuItem concrete) concrete.RaiseClicked(); + } + }; + if (sub is not null) + { + foreach (var child in sub.Children) + { + if (child is not ViewModel vm) continue; + var subSurface = CreateSurface(vm); + model.Children.Add(ToItemModel(subSurface, vm.SubContent)); + } + } + return model; + } + + private void ShowAt(int x, int y, uint align) + { + var model = EnsureMaterialized(); + int id = Win32MenuEngine.Show(_ownerHwnd, model, x, y, align); + if (id > 0) Clicked?.Invoke(); + } +} diff --git a/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs new file mode 100644 index 0000000..e877a55 --- /dev/null +++ b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs @@ -0,0 +1,89 @@ +namespace EasyWindowsApplication.Core.Menus; + +internal static class Win32MenuEngine +{ + private static int _nextId; + + internal static int NextId() => Interlocked.Increment(ref _nextId); + + internal static nint BuildMenu(MenuModel model) + { + nint hMenu = Win32.CreatePopupMenu(); + if (hMenu == 0) + throw new InvalidOperationException("CreatePopupMenu failed."); + try + { + AppendItems(hMenu, model.Items); + } + catch + { + Win32.DestroyMenu(hMenu); + throw; + } + return hMenu; + } + + internal static bool Dispatch(MenuModel model, int id) + { + if (id <= 0) return false; + var item = model.FindById(id); + if (item is null || !item.IsEnabled || item.IsSeparator) return false; + item.OnClick?.Invoke(); + return true; + } + + internal static int Show(nint hwndOwner, MenuModel model, int x, int y, uint align = TPM.LEFTALIGN | TPM.TOPALIGN) + { + nint hMenu = BuildMenu(model); + try + { + Win32.SetForegroundWindow(hwndOwner); + int id = Win32.TrackPopupMenuEx(hMenu, align | TPM.RETURNCMD, x, y, hwndOwner, 0); + Win32.PostMessageW(hwndOwner, WM.NULL, 0, 0); + if (id > 0) Dispatch(model, id); + return id; + } + finally + { + Win32.DestroyMenu(hMenu); + } + } + + private static void AppendItems(nint hMenu, List items) + { + foreach (var item in items) + { + if (item.IsSeparator) + { + Win32.AppendMenuW(hMenu, MF.SEPARATOR, 0, null); + continue; + } + if (item.Children.Count > 0) + { + nint hSub = Win32.CreatePopupMenu(); + if (hSub == 0) + throw new InvalidOperationException("CreatePopupMenu failed (submenu)."); + try + { + AppendItems(hSub, item.Children); + } + catch + { + Win32.DestroyMenu(hSub); + throw; + } + uint flags = MF.POPUP; + if (!item.IsEnabled) flags |= MF.GRAYED; + if (item.IsChecked) flags |= MF.CHECKED; + Win32.AppendMenuW(hMenu, flags, (nuint)hSub, item.Text); + } + else + { + uint flags = MF.STRING; + if (!item.IsEnabled) flags |= MF.GRAYED; + if (item.IsChecked) flags |= MF.CHECKED; + Win32.AppendMenuW(hMenu, flags, (nuint)item.Id, item.Text); + } + } + } +} diff --git a/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs b/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs index 58ee463..6914c4f 100644 --- a/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs +++ b/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs @@ -1,5 +1,6 @@ namespace EasyWindowsApplication.Core; +using System.Globalization; using EasyWindowsApplication.Share; internal sealed class ResourcesDictionaryImpl : IResourcesDictionary @@ -34,6 +35,11 @@ public ISettingsBuilder AppConfigFile(Action configure) configure(new AppConfigFileBuilderImpl(AppConfig)); return this; } + + public ISettingsBuilder Culture(CultureInfo cultureInfo) + { + throw new NotImplementedException(); + } } internal sealed class AppConfigFileConfig diff --git a/src/EasyWindowsApplication/Core/ShellEntities.cs b/src/EasyWindowsApplication/Core/ShellEntities.cs new file mode 100644 index 0000000..4a19f8a --- /dev/null +++ b/src/EasyWindowsApplication/Core/ShellEntities.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; + +namespace EasyWindowsApplication.Core; + +[StructLayout(LayoutKind.Sequential)] +internal struct NOTIFYICONIDENTIFIER +{ + public uint cbSize; + public nint hWnd; + public uint uID; + public Guid guidItem; +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +internal unsafe struct NOTIFYICONDATAW +{ + public uint cbSize; + public nint hWnd; + public uint uID; + public uint uFlags; + public uint uCallbackMessage; + public nint hIcon; + public fixed char szTip[128]; + public uint dwState; + public uint dwStateMask; + public fixed char szInfo[256]; + public uint uVersion; + public fixed char szInfoTitle[64]; + public uint dwInfoFlags; + public Guid guidItem; + public nint hBalloonIcon; +} diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs new file mode 100644 index 0000000..db73b03 --- /dev/null +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs @@ -0,0 +1,252 @@ +using System.Runtime.InteropServices; +using EasyWindowsApplication.Core.Windowing; +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Core.SystemTray; + +internal sealed class SystemTrayBroker +{ + internal const nuint HoldTimerId = 1; + internal const uint HoldThresholdMs = 500; + internal const uint HoverResetMs = 1000; + internal const uint IconId = 1; + + private const uint WM_MOUSEMOVE = 0x0200; + private const uint WM_LBUTTONDOWN = 0x0201; + private const uint WM_LBUTTONUP = 0x0202; + private const uint WM_LBUTTONDBLCLK = 0x0203; + private const uint WM_RBUTTONUP = 0x0205; + private const uint WM_MBUTTONUP = 0x0208; + + private readonly Action _dispatch; + private readonly uint _callbackMessage; + + private bool _iconAdded; + private string _tooltip = ""; + private nint _icon; + private bool _disposed; + + private Type? _lastTrigger; + private int _lastCount; + private int _lastTick; + private int _lastHoverTick; + private bool _holding; + + internal nint Hwnd { get; } + internal uint CallbackMessage => _callbackMessage; + + private SystemTrayBroker(nint hwnd, uint callbackMessage, Action dispatch) + { + Hwnd = hwnd; + _callbackMessage = callbackMessage; + _dispatch = dispatch; + } + + internal static SystemTrayBroker Create(MasterRouter router, Action dispatch) + { + nint hwnd = Windowing.Procedures.CreateHiddenWindow(router); + uint callback = Win32.RegisterWindowMessageW("EasyWindowsApplication.Tray.v1"); + var broker = new SystemTrayBroker(hwnd, callback, dispatch); + router.RegisterHandler(hwnd, callback, broker.OnCallback); + router.RegisterHandler(hwnd, WM.CONTEXTMENU, broker.OnContextMenu); + router.RegisterHandler(hwnd, WM.TIMER, broker.OnTimer); + router.RegisterHandler(hwnd, WM.DESTROY, broker.OnDestroy); + return broker; + } + + internal bool AddIcon(nint icon) + { + _icon = icon; + var data = NewData(NIF.MESSAGE | NIF.ICON | NIF.TIP); + CopyStrings(ref data, _tooltip, null, null); + if (!Win32.Shell_NotifyIconW(NIM.ADD, ref data)) return false; + _iconAdded = true; + data.uVersion = NOTIFYICON.VERSION_4; + Win32.Shell_NotifyIconW(NIM.SETVERSION, ref data); + return true; + } + + internal void RemoveIcon() + { + if (!_iconAdded) return; + var data = NewData(0); + Win32.Shell_NotifyIconW(NIM.DELETE, ref data); + _iconAdded = false; + } + + internal void UpdateTooltip(string tooltip) + { + _tooltip = tooltip; + if (!_iconAdded) return; + var data = NewData(NIF.TIP); + CopyStrings(ref data, tooltip, null, null); + Win32.Shell_NotifyIconW(NIM.MODIFY, ref data); + } + + internal void ShowBalloon(string title, string text) + { + if (!_iconAdded) return; + var data = NewData(NIF.INFO); + CopyStrings(ref data, null, text, title); + Win32.Shell_NotifyIconW(NIM.MODIFY, ref data); + } + + internal void HideBalloon() + { + if (!_iconAdded) return; + var data = NewData(NIF.INFO); + Win32.Shell_NotifyIconW(NIM.MODIFY, ref data); + } + + internal void Shutdown() + { + if (_disposed) return; + _disposed = true; + Win32.KillTimer(Hwnd, HoldTimerId); + RemoveIcon(); + Windowing.Win32.DestroyWindow(Hwnd); + HandleRegistry.UnregisterRouter(Hwnd); + } + + internal void SetVisible(bool visible) + { + if (_disposed) return; + if (visible) + { + if (!_iconAdded) AddIcon(_icon); + } + else + { + RemoveIcon(); + } + } + + private nint OnCallback(nint wParam, nint lParam) + { + switch ((uint)lParam) + { + case WM_MOUSEMOVE: OnHover(); break; + case WM_LBUTTONDOWN: OnButtonDown(); break; + case WM_LBUTTONUP: OnButtonUp(); break; + case WM_LBUTTONDBLCLK: Fire(typeof(MainDoubleTap)); break; + case WM_RBUTTONUP: Fire(typeof(AlternativeTap1)); break; + case WM_MBUTTONUP: Fire(typeof(AlternativeTap2)); break; + case var m when m == NIN.SELECT || m == NIN.KEYSELECT: Fire(typeof(MainTap)); break; + case var m when m == NIN.POPUPOPEN: OnHover(); break; + } + return 0; + } + + private nint OnContextMenu(nint wParam, nint lParam) + { + if (lParam != -1) return 0; + Fire(typeof(KeyMenu)); + if (Win32.GetKeyState(VK.SHIFT) < 0) + _dispatch(typeof(Chord), 1); + return 0; + } + + private nint OnTimer(nint wParam, nint lParam) + { + if ((nuint)wParam != HoldTimerId) return 0; + Win32.KillTimer(Hwnd, HoldTimerId); + _holding = true; + Fire(typeof(Holding)); + return 0; + } + + private nint OnDestroy(nint wParam, nint lParam) + { + Win32.KillTimer(Hwnd, HoldTimerId); + RemoveIcon(); + HandleRegistry.UnregisterRouter(Hwnd); + return 0; + } + + private void OnButtonDown() + { + _holding = false; + Win32.SetTimer(Hwnd, HoldTimerId, HoldThresholdMs, 0); + } + + private void OnButtonUp() + { + Win32.KillTimer(Hwnd, HoldTimerId); + if (_holding) + { + _holding = false; + Fire(typeof(LongTap)); + } + else + { + Fire(typeof(MainTap)); + } + } + + private void OnHover() + { + int now = Environment.TickCount; + if (_lastHoverTick != 0 && unchecked(now - _lastHoverTick) < (int)HoverResetMs) return; + _lastHoverTick = now; + Fire(typeof(Hover)); + } + + private void Fire(Type trigger) + { + int count = 1; + if (IsChainable(trigger) && trigger == _lastTrigger) + { + int now = Environment.TickCount; + if (unchecked(now - _lastTick) <= (int)Win32.GetDoubleClickTime()) + count = Math.Min(_lastCount + 1, 10); + } + if (IsChainable(trigger)) + { + _lastTrigger = trigger; + _lastCount = count; + _lastTick = Environment.TickCount; + } + else + { + _lastTrigger = null; + _lastCount = 0; + } + _dispatch(trigger, count); + } + + private static bool IsChainable(Type trigger) + => trigger == typeof(MainTap) + || trigger == typeof(AlternativeTap1) + || trigger == typeof(AlternativeTap2) + || trigger == typeof(LongTap); + + private NOTIFYICONDATAW NewData(uint flags) + { + return new NOTIFYICONDATAW + { + cbSize = (uint)Marshal.SizeOf(), + hWnd = Hwnd, + uID = IconId, + uFlags = flags, + uCallbackMessage = _callbackMessage, + hIcon = _icon + }; + } + + private static unsafe void CopyStrings(ref NOTIFYICONDATAW data, string? tip, string? info, string? infoTitle) + { + fixed (NOTIFYICONDATAW* p = &data) + { + if (tip is not null) CopyText(p->szTip, 128, tip); + if (info is not null) CopyText(p->szInfo, 256, info); + if (infoTitle is not null) CopyText(p->szInfoTitle, 64, infoTitle); + } + } + + private static unsafe void CopyText(char* dst, int capacity, string text) + { + int n = Math.Min(text.Length, capacity - 1); + for (int i = 0; i < n; i++) dst[i] = text[i]; + dst[n] = '\0'; + } +} diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs new file mode 100644 index 0000000..b5765e8 --- /dev/null +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs @@ -0,0 +1,114 @@ +using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Core.SystemTray; + +internal sealed class SystemTrayImpl : ISystemTray +{ + private static readonly Type[] TapCounts = + { + typeof(OneTap), typeof(TwoTap), typeof(ThreeTap), typeof(FourTap), typeof(FiveTap), + typeof(SixTap), typeof(SevenTap), typeof(EightTap), typeof(NineTap), typeof(TenTap) + }; + + private readonly SystemTrayBroker _broker; + private readonly Dictionary> _spatial = new(); + private readonly Dictionary<(Type Trigger, Type Count), List> _counted = new(); + private readonly Dictionary> _nonSpatial = new(); + private string _tooltip = ""; + + internal SystemTrayBroker Broker => _broker; + + internal SystemTrayImpl(SystemTrayBroker broker) => _broker = broker; + + public string Name { get; set; } = "SystemTray"; + public LayoutLength? LayoutWidth { get; set; } + public LayoutLength? LayoutHeight { get; set; } + public LayoutOptions LayoutOptions { get; set; } = new(); + public Thickness Margin { get; set; } + public Thickness Padding { get; set; } + public DockPosition Dock { get; set; } + public int GridRow { get; set; } + public int GridColumn { get; set; } + public int GridRowSpan { get; set; } + public int GridColumnSpan { get; set; } + public Color? BackgroundColor { get; set; } + + public event Action? Clicked; + + public void OnClick(Action handler) => Clicked += handler; + + public void Visibility(bool visible) => _broker.SetVisible(visible); + + public ISystemTray Tooltip(string text) + { + _tooltip = text; + _broker.UpdateTooltip(text); + return this; + } + + public ISystemTray TooltipShow() + { + _broker.ShowBalloon(Name, _tooltip); + return this; + } + + public ISystemTray TooltipHide() + { + _broker.HideBalloon(); + return this; + } + + public ISystemTray OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + { + Add(_spatial, typeof(TTrigger), handler); + return this; + } + + public ISystemTray OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount + { + var key = (typeof(TTrigger), typeof(TCount)); + if (!_counted.TryGetValue(key, out var list)) + { + list = new List(); + _counted[key] = list; + } + list.Add(handler); + return this; + } + + public ISystemTray OnInputWithoutSpatialPosition(Action handler) + where TTrigger : WithoutSpatialPositionTrigger + { + Add(_nonSpatial, typeof(TTrigger), handler); + return this; + } + + internal void DispatchTrigger(Type trigger, int count) + { + if (trigger == typeof(MainTap)) Clicked?.Invoke(); + if (_spatial.TryGetValue(trigger, out var list)) + foreach (var handler in list.ToArray()) handler(); + if (count >= 1 && count <= TapCounts.Length) + { + var key = (trigger, TapCounts[count - 1]); + if (_counted.TryGetValue(key, out var counted)) + foreach (var handler in counted.ToArray()) handler(); + } + if (_nonSpatial.TryGetValue(trigger, out var nonSpatial)) + foreach (var handler in nonSpatial.ToArray()) handler(); + } + + private static void Add(Dictionary> map, Type trigger, Action handler) + { + if (!map.TryGetValue(trigger, out var list)) + { + list = new List(); + map[trigger] = list; + } + list.Add(handler); + } +} diff --git a/src/EasyWindowsApplication/Core/TaskbarList.cs b/src/EasyWindowsApplication/Core/TaskbarList.cs new file mode 100644 index 0000000..ddc38b4 --- /dev/null +++ b/src/EasyWindowsApplication/Core/TaskbarList.cs @@ -0,0 +1,57 @@ +using System.Runtime.InteropServices; + +namespace EasyWindowsApplication.Core; + +internal static partial class TaskbarList +{ + [ComImport] + [Guid("56FDF344-FD6D-11D0-958A-006097C9A090")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface ITaskbarList + { + void HrInit(); + void AddTab(nint hwnd); + void DeleteTab(nint hwnd); + void ActivateTab(nint hwnd); + void SetActiveAlt(nint hwnd); + } + + [LibraryImport("ole32.dll")] + private static partial int CoCreateInstance( + ref Guid rclsid, nint pUnkOuter, uint dwClsContext, ref Guid riid, out nint ppv); + + internal static bool SetVisible(nint hwnd, bool visible) + { + if (hwnd == 0) return false; + try + { + var clsid = new Guid("56FDF344-FD6D-11D0-958A-006097C9A090"); + var iid = new Guid("56FDF342-FD6D-11D0-958A-006097C9A090"); + int hr = CoCreateInstance(ref clsid, 0, 1, ref iid, out nint ppv); + if (hr != 0 || ppv == 0) return false; + try + { + var list = (ITaskbarList)Marshal.GetObjectForIUnknown(ppv); + try + { + list.HrInit(); + if (visible) list.AddTab(hwnd); + else list.DeleteTab(hwnd); + return true; + } + finally + { + Marshal.ReleaseComObject(list); + } + } + finally + { + Marshal.Release(ppv); + } + } + catch + { + return false; + } + } +} diff --git a/src/EasyWindowsApplication/Core/Win32.cs b/src/EasyWindowsApplication/Core/Win32.cs index 15147cd..d313cd8 100644 --- a/src/EasyWindowsApplication/Core/Win32.cs +++ b/src/EasyWindowsApplication/Core/Win32.cs @@ -215,4 +215,60 @@ internal static partial class Win32 [LibraryImport("user32.dll", EntryPoint = "SetWindowTextW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SetWindowText(nint hWnd, string lpString); + + // ── Window Messages ── + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + internal static partial uint RegisterWindowMessageW(string lpString); + + // ── Timers ── + [LibraryImport("user32.dll")] + internal static partial nuint SetTimer(nint hWnd, nuint nIDEvent, uint uElapse, nint lpTimerFunc); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool KillTimer(nint hWnd, nuint uIDEvent); + + // ── Keyboard state ── + [LibraryImport("user32.dll")] + internal static partial short GetKeyState(int nVirtKey); + + [LibraryImport("user32.dll")] + internal static partial uint GetDoubleClickTime(); + + // ── Menus (HMENU) ── + [LibraryImport("user32.dll")] + internal static partial nint CreatePopupMenu(); + + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool AppendMenuW(nint hMenu, uint uFlags, nuint uIDNewItem, string? lpNewItem); + + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool InsertMenuW(nint hMenu, uint uPosition, uint uFlags, nuint uIDNewItem, string? lpNewItem); + + [LibraryImport("user32.dll")] + internal static partial int TrackPopupMenuEx(nint hMenu, uint uFlags, int x, int y, nint hwnd, nint lptpm); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool DestroyMenu(nint hMenu); + + [LibraryImport("user32.dll")] + internal static partial int GetMenuItemCount(nint hMenu); + + [LibraryImport("user32.dll")] + internal static partial uint GetMenuState(nint hMenu, uint uId, uint uFlags); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetCursorPos(out POINT lpPoint); + + // ── Shell NotifyIcon ── + [LibraryImport("shell32.dll", StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Shell_NotifyIconW(uint dwMessage, ref NOTIFYICONDATAW lpData); + + [LibraryImport("shell32.dll")] + internal static partial int Shell_NotifyIconGetRect(ref NOTIFYICONIDENTIFIER identifier, out RECT iconLocation); } diff --git a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs index ee1a5ee..8bdd1fb 100644 --- a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs @@ -109,6 +109,11 @@ public void Show() } public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + public void Visibility(bool visible) + { + if (visible) Show(); + else Hide(); + } internal void RaiseLoaded() { @@ -168,7 +173,11 @@ private static void MaterializeChildren(ContentModel model, nint parentHwnd, Han vm.Control = control; } - if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(control, out var factory) || factory is null) + // Las superficies sin HWND (IMenu, IMenuItem) no se materializan aquí; + // las materializa MenuSurface (Fase 1/2). + if (control is not IControl nativeControl) continue; + + if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(nativeControl, out var factory) || factory is null) { if (vm.ControlType is not null && ControlActivatorRegistry.Shared.TryGetFactory(vm.ControlType) is { } fallback) factory = fallback; @@ -176,7 +185,7 @@ private static void MaterializeChildren(ContentModel model, nint parentHwnd, Han throw new InvalidOperationException($"No handle factory registered for control '{control.GetType().Name}' (name='{control.Name}')."); } - nint hwnd = factory.CreateHandle(parentHwnd, control, registry); + nint hwnd = factory.CreateHandle(parentHwnd, nativeControl, registry); if (hwnd == 0) throw new InvalidOperationException($"CreateHandle failed for control '{control.Name}' ({control.GetType().Name})"); diff --git a/src/EasyWindowsApplication/Core/Windowing/Procedures.cs b/src/EasyWindowsApplication/Core/Windowing/Procedures.cs index 64ee4bc..8c14968 100644 --- a/src/EasyWindowsApplication/Core/Windowing/Procedures.cs +++ b/src/EasyWindowsApplication/Core/Windowing/Procedures.cs @@ -154,4 +154,67 @@ internal static unsafe nint CreateAlternativeWindow( return hwnd; } + + internal static nint LoadAppIcon() + { + nint hInstance = Win32.GetModuleHandleW(0); + int iconResId = ResolveIconResourceId(hInstance); + nint hIcon = Win32.LoadImageW( + hInstance, (nint)iconResId, IMAGE.ICON, + 0, 0, + LR.DEFAULTSIZE | LR.CREATEDIBSECTION); + if (hIcon == 0) + hIcon = Win32.LoadIconW(0, (nint)RT_MAINICON); + return hIcon; + } + + internal static unsafe nint CreateHiddenWindow(MasterRouter router) + { + nint hInstance = Win32.GetModuleHandleW(0); + + string className = $"EasyWinTray_{Guid.NewGuid():N}"; + nint classNamePtr = Marshal.StringToHGlobalUni(className); + + nint wndProcPtr = (nint)(delegate* unmanaged[Stdcall])&MasterRouter.WndProcTrampoline; + + var wndClass = new WNDCLASSEXW + { + cbSize = (uint)Marshal.SizeOf(), + style = CS.HREDRAW | CS.VREDRAW | CS.DBLCLKS, + lpfnWndProc = wndProcPtr, + cbClsExtra = 0, + cbWndExtra = 0, + hInstance = hInstance, + hIcon = 0, + hCursor = 0, + hbrBackground = 0, + lpszMenuName = 0, + lpszClassName = classNamePtr, + hIconSm = 0 + }; + + ushort atom = Win32.RegisterClassExW(ref wndClass); + if (atom == 0) + { + Marshal.FreeHGlobal(classNamePtr); + throw new InvalidOperationException($"RegisterClassExW failed: {Marshal.GetLastWin32Error()}"); + } + + nint titlePtr = Marshal.StringToHGlobalUni("EasyWinTray"); + nint hwnd = Win32.CreateWindowExW( + 0, classNamePtr, titlePtr, + WS.POPUP, + 0, 0, 0, 0, + 0, 0, hInstance, 0); + + Marshal.FreeHGlobal(classNamePtr); + Marshal.FreeHGlobal(titlePtr); + + if (hwnd == 0) + throw new InvalidOperationException($"CreateWindowExW failed: {Marshal.GetLastWin32Error()}"); + + HandleRegistry.RegisterRouter(hwnd, router); + + return hwnd; + } } diff --git a/src/EasyWindowsApplication/Core/Windowing/Win32.cs b/src/EasyWindowsApplication/Core/Windowing/Win32.cs index e8e49a3..8e0db7b 100644 --- a/src/EasyWindowsApplication/Core/Windowing/Win32.cs +++ b/src/EasyWindowsApplication/Core/Windowing/Win32.cs @@ -39,6 +39,9 @@ internal static partial nint CreateWindowExW( [LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] internal static partial nint LoadImageW(nint hInst, nint name, uint type, int cx, int cy, uint fuLoad); + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + internal static partial nint LoadIconW(nint hInstance, nint lpIconName); + [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool GetClientRect(nint hWnd, out RECT lpRect); diff --git a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs index 2db81da..ef4630c 100644 --- a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs @@ -96,6 +96,11 @@ internal WindowImpl(nint hwnd, string name, string title, float width, float hei public void Show() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.SHOW); public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + public void Visibility(bool visible) + { + if (visible) Show(); + else Hide(); + } public void Center() { @@ -444,8 +449,12 @@ private static void MaterializeChildren(ContentModel model, nint parentHwnd, Han vm.Control = control; } + // Las superficies sin HWND (IMenu, IMenuItem) no se materializan aquí; + // las materializa MenuSurface (Fase 1/2). + if (control is not IControl nativeControl) continue; + // Resolver factory por tipo concreto/interfaz - if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(control, out var factory) || factory is null) + if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(nativeControl, out var factory) || factory is null) { if (vm.ControlType is not null && ControlActivatorRegistry.Shared.TryGetFactory(vm.ControlType) is { } fallback && fallback is not null) factory = fallback; @@ -453,7 +462,7 @@ private static void MaterializeChildren(ContentModel model, nint parentHwnd, Han throw new InvalidOperationException($"No handle factory registered for control '{control.GetType().Name}' (name='{control.Name}'). Registre un INativeHandleFactory para ese tipo."); } - nint hwnd = factory.CreateHandle(parentHwnd, control, registry); + nint hwnd = factory.CreateHandle(parentHwnd, nativeControl, registry); if (hwnd == 0) throw new InvalidOperationException($"CreateHandle failed for control '{control.Name}' ({control.GetType().Name})"); diff --git a/src/EasyWindowsApplication/Share/IAlternativeWindow.cs b/src/EasyWindowsApplication/Share/IAlternativeWindow.cs index d883fe8..b48e794 100644 --- a/src/EasyWindowsApplication/Share/IAlternativeWindow.cs +++ b/src/EasyWindowsApplication/Share/IAlternativeWindow.cs @@ -8,4 +8,5 @@ public interface IAlternativeWindow : IBaseWindow int Width { get; set; } int Height { get; set; } WindowPositionOnScreen PositionMode { get; set; } + void Visibility(bool visible); } diff --git a/src/EasyWindowsApplication/Share/IAppBehavior.cs b/src/EasyWindowsApplication/Share/IAppBehavior.cs new file mode 100644 index 0000000..580d0bf --- /dev/null +++ b/src/EasyWindowsApplication/Share/IAppBehavior.cs @@ -0,0 +1,11 @@ +namespace EasyWindowsApplication.Share; + +public interface IAppBehavior +{ + IAppBehavior OnLaunched(Action handler); + IAppBehavior OnLaunched(Action handler); + IAppBehavior OnTerminating(Action handler); + IAppBehavior OnTerminated(Action handler); + + IAppBehavior TaskbarButtonVisibility(bool visible); +} \ No newline at end of file diff --git a/src/EasyWindowsApplication/Share/IBehaviorBuilder.cs b/src/EasyWindowsApplication/Share/IBehaviorBuilder.cs index f26de30..fa7fbfa 100644 --- a/src/EasyWindowsApplication/Share/IBehaviorBuilder.cs +++ b/src/EasyWindowsApplication/Share/IBehaviorBuilder.cs @@ -1,3 +1,7 @@ namespace EasyWindowsApplication.Share; -public interface IBehaviorBuilder { } +public interface IBehaviorBuilder +{ + IAppBehavior WindowsApplication { get; } + ISystemTray SystemTray { get; } +} diff --git a/src/EasyWindowsApplication/Share/IChildrenBuilder.cs b/src/EasyWindowsApplication/Share/IChildrenBuilder.cs index e2c7184..b00fc10 100644 --- a/src/EasyWindowsApplication/Share/IChildrenBuilder.cs +++ b/src/EasyWindowsApplication/Share/IChildrenBuilder.cs @@ -1,11 +1,10 @@ -using EasyWindowsApplication.Win32ControlsModule.Frontend; - -namespace EasyWindowsApplication.Share; +namespace EasyWindowsApplication.Share; public interface IChildrenBuilder { - IChildrenBuilder View(Action> configure) where T : class, IControl; - IChildrenBuilder View(Func, View> configure) where T : class, IControl; + IChildrenBuilder View() where T : class, IViewSurface; + IChildrenBuilder View(Action> configure) where T : class, IViewSurface; + IChildrenBuilder View(Func, View> configure) where T : class, IViewSurface; IChildrenBuilder View(Action configure); IChildrenBuilder Row(int row); diff --git a/src/EasyWindowsApplication/Share/ILayoutBuilder.cs b/src/EasyWindowsApplication/Share/ILayoutBuilder.cs index fedb52d..2e4b442 100644 --- a/src/EasyWindowsApplication/Share/ILayoutBuilder.cs +++ b/src/EasyWindowsApplication/Share/ILayoutBuilder.cs @@ -5,4 +5,7 @@ public interface ILayoutBuilder { ILayoutBuilderAfterWindow Window(); ILayoutBuilderAfterWindow Window(Action configure); + ILayoutBuilderAfterWindow AlternativeWindow(); + ILayoutBuilderAfterWindow AlternativeWindow(Action configure); + ILayoutBuilderAfterWindow AlternativeWindow(Action configure) where T : class, IViewSurface; } diff --git a/src/EasyWindowsApplication/Share/ILayoutBuilderAfterWindow.cs b/src/EasyWindowsApplication/Share/ILayoutBuilderAfterWindow.cs index 49075bd..9812fa8 100644 --- a/src/EasyWindowsApplication/Share/ILayoutBuilderAfterWindow.cs +++ b/src/EasyWindowsApplication/Share/ILayoutBuilderAfterWindow.cs @@ -5,4 +5,5 @@ public interface ILayoutBuilderAfterWindow { ILayoutBuilderAfterWindow AlternativeWindow(); ILayoutBuilderAfterWindow AlternativeWindow(Action configure); + ILayoutBuilderAfterWindow AlternativeWindow(Action configure) where T : class, IViewSurface; } diff --git a/src/EasyWindowsApplication/Share/IMenu.cs b/src/EasyWindowsApplication/Share/IMenu.cs new file mode 100644 index 0000000..e50d71c --- /dev/null +++ b/src/EasyWindowsApplication/Share/IMenu.cs @@ -0,0 +1,10 @@ +namespace EasyWindowsApplication.Share; + +public interface IMenu : IViewSurface +{ + void Show(); + void ShowAtCursor(); + void ShowAtIcon(); + void Hide(); + void Close(); +} diff --git a/src/EasyWindowsApplication/Share/IMenuItem.cs b/src/EasyWindowsApplication/Share/IMenuItem.cs new file mode 100644 index 0000000..529de17 --- /dev/null +++ b/src/EasyWindowsApplication/Share/IMenuItem.cs @@ -0,0 +1,9 @@ +using EasyWindowsApplication.Win32ControlsModule.Frontend; + +namespace EasyWindowsApplication.Share; + +public interface IMenuItem : IViewSurface, IText +{ + bool IsEnabled { get; set; } + bool IsChecked { get; set; } +} diff --git a/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs b/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs new file mode 100644 index 0000000..5de38be --- /dev/null +++ b/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs @@ -0,0 +1,9 @@ +namespace EasyWindowsApplication.Share; + +public interface IMenuItemSeparator : IViewSurface +{ + // Sin miembros propios en v1 (ver proposal-imenuitemseparator.md). + // Name (heredado) sirve para orden + lookup bh.*. + // OnClick/Clicked (heredados) son inertes: el OS no permite + // seleccionar un separador (MF_SEPARATOR, sin ID ni comando). +} diff --git a/src/EasyWindowsApplication/Share/IResourcesDictionary.cs b/src/EasyWindowsApplication/Share/IResourcesDictionary.cs index ea4db29..5791022 100644 --- a/src/EasyWindowsApplication/Share/IResourcesDictionary.cs +++ b/src/EasyWindowsApplication/Share/IResourcesDictionary.cs @@ -1,4 +1,6 @@ -namespace EasyWindowsApplication.Share; +using System.Globalization; + +namespace EasyWindowsApplication.Share; public interface IResourcesDictionary { @@ -8,6 +10,7 @@ public interface IResourcesDictionary public interface ISettingsBuilder { + ISettingsBuilder Culture(CultureInfo cultureInfo); ISettingsBuilder UseWinApi(); ISettingsBuilder AppConfigFile(Action configure); } diff --git a/src/EasyWindowsApplication/Share/ISystemTray.cs b/src/EasyWindowsApplication/Share/ISystemTray.cs new file mode 100644 index 0000000..b7da260 --- /dev/null +++ b/src/EasyWindowsApplication/Share/ISystemTray.cs @@ -0,0 +1,17 @@ +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Share; + +public interface ISystemTray : IViewSurface +{ + ISystemTray Tooltip(string text); + ISystemTray TooltipShow(); + ISystemTray TooltipHide(); + ISystemTray OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger; + ISystemTray OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount; + ISystemTray OnInputWithoutSpatialPosition(Action handler) + where TTrigger : WithoutSpatialPositionTrigger; +} diff --git a/src/EasyWindowsApplication/Share/IViewSurface.cs b/src/EasyWindowsApplication/Share/IViewSurface.cs new file mode 100644 index 0000000..89ee3dd --- /dev/null +++ b/src/EasyWindowsApplication/Share/IViewSurface.cs @@ -0,0 +1,31 @@ +namespace EasyWindowsApplication.Share; + +public interface IViewSurface +{ + string Name { get; set; } + LayoutLength? LayoutWidth { get; set; } + LayoutLength? LayoutHeight { get; set; } + LayoutOptions LayoutOptions { get; set; } + Thickness Margin { get; set; } + Thickness Padding { get; set; } + DockPosition Dock { get; set; } + + int GridRow { get; set; } + int GridColumn { get; set; } + int GridRowSpan { get; set; } + int GridColumnSpan { get; set; } + + Color? BackgroundColor { get; set; } + + event Action? Clicked; + void OnClick(Action handler); + + void Visibility(bool visible) + { + if (this is IBaseWindow window) + { + if (visible) window.Show(); + else window.Hide(); + } + } +} diff --git a/src/EasyWindowsApplication/Share/IWindow.cs b/src/EasyWindowsApplication/Share/IWindow.cs index 4aa5dc0..ae306a6 100644 --- a/src/EasyWindowsApplication/Share/IWindow.cs +++ b/src/EasyWindowsApplication/Share/IWindow.cs @@ -17,6 +17,7 @@ public interface IWindow : IBaseWindow void Minimize(); void Restore(); void Focus(); + void Visibility(bool visible); (int X, int Y) ScrollOffset { get; } void ScrollTo(int x, int y); diff --git a/src/EasyWindowsApplication/Share/IWindowConfig.cs b/src/EasyWindowsApplication/Share/IWindowConfig.cs index 2e4eae0..8933e0d 100644 --- a/src/EasyWindowsApplication/Share/IWindowConfig.cs +++ b/src/EasyWindowsApplication/Share/IWindowConfig.cs @@ -9,6 +9,7 @@ public interface IWindowConfig IWindowConfig Position(WindowPositionOnScreen position); IWindowConfig Background(Color color); IWindowConfig Scroll(Action configure); + IWindowConfig SystemTray(Action configure); IWindowContentConfig Content(Action configure) where TLayout : IStackLayout; IWindowContentConfig Content(Action configure); } diff --git a/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs b/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs index 8330329..43d06c0 100644 --- a/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs +++ b/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs @@ -15,6 +15,11 @@ internal interface IBehaviorServicesController T GetWindow(string name) where T : IBaseWindow; } + internal interface ISurfaceServicesController + { + T GetSurface(string name) where T : IViewSurface; + } + internal static void SetController(IBehaviorServicesController? controller) => _controller = controller; public static T Get(string name) where T : IControl @@ -22,4 +27,7 @@ public static T Get(string name) where T : IControl public static T GetWindow(string name) where T : IBaseWindow => _controller!.GetWindow(name); + + public static T GetSurface(string name) where T : IViewSurface + => ((ISurfaceServicesController)_controller!).GetSurface(name); } \ No newline at end of file diff --git a/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs b/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs new file mode 100644 index 0000000..2576b5b --- /dev/null +++ b/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs @@ -0,0 +1,17 @@ +namespace EasyWindowsApplication.Share.Input; + +public interface ISpatialPositionTrigger { } + +public readonly struct Hover : ISpatialPositionTrigger { } + +public readonly struct MainTap : ISpatialPositionTrigger { } + +public readonly struct MainDoubleTap : ISpatialPositionTrigger { } + +public readonly struct AlternativeTap1 : ISpatialPositionTrigger { } + +public readonly struct AlternativeTap2 : ISpatialPositionTrigger { } + +public readonly struct LongTap : ISpatialPositionTrigger { } + +public readonly struct Holding : ISpatialPositionTrigger { } diff --git a/src/EasyWindowsApplication/Share/Input/KeyPrimitives.cs b/src/EasyWindowsApplication/Share/Input/KeyPrimitives.cs new file mode 100644 index 0000000..3a24936 --- /dev/null +++ b/src/EasyWindowsApplication/Share/Input/KeyPrimitives.cs @@ -0,0 +1,43 @@ +namespace EasyWindowsApplication.Share.Input; + +public interface IKey { } + +public interface IKeyMod { } + +public readonly struct KeyShift : IKeyMod { } + +public readonly struct KeyCtrl : IKeyMod { } + +public readonly struct KeyAlt : IKeyMod { } + +public readonly struct KeyWin : IKeyMod { } + +public readonly struct KeyF1 : IKey { } + +public readonly struct KeyF2 : IKey { } + +public readonly struct KeyF3 : IKey { } + +public readonly struct KeyF4 : IKey { } + +public readonly struct KeyF5 : IKey { } + +public readonly struct KeyF6 : IKey { } + +public readonly struct KeyF7 : IKey { } + +public readonly struct KeyF8 : IKey { } + +public readonly struct KeyF9 : IKey { } + +public readonly struct KeyF10 : IKey { } + +public readonly struct KeyF11 : IKey { } + +public readonly struct KeyF12 : IKey { } + +public readonly struct KeyEnter : IKey { } + +public readonly struct KeySpace : IKey { } + +public readonly struct KeyEscape : IKey { } diff --git a/src/EasyWindowsApplication/Share/Input/TapCount.cs b/src/EasyWindowsApplication/Share/Input/TapCount.cs new file mode 100644 index 0000000..8a1f33a --- /dev/null +++ b/src/EasyWindowsApplication/Share/Input/TapCount.cs @@ -0,0 +1,23 @@ +namespace EasyWindowsApplication.Share.Input; + +public interface ITapCount { } + +public readonly struct OneTap : ITapCount { } + +public readonly struct TwoTap : ITapCount { } + +public readonly struct ThreeTap : ITapCount { } + +public readonly struct FourTap : ITapCount { } + +public readonly struct FiveTap : ITapCount { } + +public readonly struct SixTap : ITapCount { } + +public readonly struct SevenTap : ITapCount { } + +public readonly struct EightTap : ITapCount { } + +public readonly struct NineTap : ITapCount { } + +public readonly struct TenTap : ITapCount { } diff --git a/src/EasyWindowsApplication/Share/Input/WithoutSpatialPositionTrigger.cs b/src/EasyWindowsApplication/Share/Input/WithoutSpatialPositionTrigger.cs new file mode 100644 index 0000000..124a229 --- /dev/null +++ b/src/EasyWindowsApplication/Share/Input/WithoutSpatialPositionTrigger.cs @@ -0,0 +1,11 @@ +namespace EasyWindowsApplication.Share.Input; + +public interface WithoutSpatialPositionTrigger { } + +public readonly struct KeyMenu : WithoutSpatialPositionTrigger { } + +public readonly struct Chord : WithoutSpatialPositionTrigger + where TMod : IKeyMod + where TKey : IKey +{ +} diff --git a/src/EasyWindowsApplication/Share/View.cs b/src/EasyWindowsApplication/Share/View.cs index 7342fe9..9685146 100644 --- a/src/EasyWindowsApplication/Share/View.cs +++ b/src/EasyWindowsApplication/Share/View.cs @@ -22,7 +22,7 @@ internal ViewBase() { } } } -public sealed class View where T : class, IControl +public sealed class View where T : class, IViewSurface { private readonly List> _configure = new(); internal string? PendingName { get; private set; } @@ -175,12 +175,12 @@ internal void Apply(T instance) foreach (var a in _configure) a(instance); } - internal Action BuildConfigure() + internal Action BuildConfigure() { var snapshot = _configure.ToArray(); - return control => + return surface => { - var typed = (T)control; + var typed = (T)surface; foreach (var a in snapshot) a(typed); }; } diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IControl.cs b/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IControl.cs index 1041dbd..f89221d 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IControl.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IControl.cs @@ -2,25 +2,9 @@ namespace EasyWindowsApplication.Win32ControlsModule.Frontend; -public interface IControl : IBaseWindow +public interface IControl : IBaseWindow, IViewSurface { new string Name { get; set; } - LayoutLength? LayoutWidth { get; set; } - LayoutLength? LayoutHeight { get; set; } - LayoutOptions LayoutOptions { get; set; } - Thickness Margin { get; set; } - Thickness Padding { get; set; } - DockPosition Dock { get; set; } - - int GridRow { get; set; } - int GridColumn { get; set; } - int GridRowSpan { get; set; } - int GridColumnSpan { get; set; } - - Color? BackgroundColor { get; set; } - - event Action? Clicked; - void OnClick(Action handler); } internal interface IClickEventSource diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index 98a74e8..bb749c0 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -1,34 +1,58 @@ using EasyWindowsApplication; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; using EasyWindowsApplication.Win32ControlsModule.Frontend; -WindowsApplication.Layout(ly => ly.Window()).Initialize(); +//WindowsApplication.Layout(ly => ly.Window()).Initialize(); -//int counter = 0; - -//WindowsApplication -// .Resources(rd => rd.Setting(st => st.UseWinApi())) -// .Layout(ly => ly -// .Window(iw => iw -// .Name("MainWindow") -// .Title("Easy Win App") -// .Dimensions(420, 280) -// .Position(WindowPositionOnScreen.Center) -// .Content(c => c -// .Children(ch => ch -// .View(btn => btn -// .Name("BtnIncrement") -// .Text("Click me") -// ) -// ) -// ) -// ) -// ) -// .Behavior(bh => bh -// .BtnIncrement.OnClick(() => -// { -// counter++; -// bh.BtnIncrement.Text = $"Click: {counter}"; -// }) -// ) -// .Initialize(); +WindowsApplication + .Resources(rd => rd.Setting(st => { + st.UseWinApi(); + st.Culture(new System.Globalization.CultureInfo("en-us")); + })) + .Layout(ly => + { + ly.Window(iw => iw + .SystemTray(st => + { + st.Tooltip(""); + }) + .Name("MainWindow") + .Title("Easy Win App") + .Dimensions(420, 280) + .Position(WindowPositionOnScreen.Center) + .Content(c => c + .Children(ch => ch + .View(btn => btn + .Name("BtnIncrement") + .Text("Click me") + ) + ) + ) + ); + ly.AlternativeWindow(m => m + .Name("MySystemTrayMenu") + .Content(c => c + .Children(ch => + { + ch.View(mi => mi.Name("Mi1").Text("Show main window")); + ch.View(); + ch.View(mi => mi.Name("Mi2").Text("Exit")); + }) + ) + ); + }) + .Behavior(bh => + { + bh.WindowsApplication.OnLaunched(wa => + { + wa.TaskbarButtonVisibility(false); + bh.MainWindow.Visibility(false); + bh.SystemTray.Visibility(true); + }); + bh.SystemTray.OnInputWithSpatialPosition(() => { bh.SystemTray.TooltipShow(); }); + bh.SystemTray.OnInputWithSpatialPosition(() => { bh.MySystemTrayMenu.Show(); }); + bh.SystemTray.OnInputWithoutSpatialPosition(() => { bh.MySystemTrayMenu.Show(); }); + bh.SystemTray.OnInputWithoutSpatialPosition>(() => { bh.MySystemTrayMenu.Show(); }); + }) + .Initialize(); diff --git a/src/Sample/Sample.csproj b/src/Sample/Sample.csproj index 1d6b118..9b2f736 100644 --- a/src/Sample/Sample.csproj +++ b/src/Sample/Sample.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/system_design.md b/src/system_design.md index c12b1db..6e4e329 100644 --- a/src/system_design.md +++ b/src/system_design.md @@ -28,7 +28,7 @@ WindowsApplication.Resources(...).Layout(...).Initialize(); - Cuando se pone `.` despues de `Behavior(...)`, el IntelliSense debe mostrar `Initialize`. - Cuando se pone `.` despues de `Initialize()`, el IntelliSense no debe recomendar nada. -> **Pipeline `Initialize()`:** `Application.Initialize()` ejecuta en orden: `1) UiDefaultsProvider.Set(new Win32UiDefaults())` → `2) InitCommonControlsEx(STANDARD_CLASSES)` → `3) ControlActivatorRegistry.EnsureInitialized()` → `4) new MasterRouter(registry)` → `5) foreach window: CreateMainWindow/CreateAlternativeWindow + MaterializeContent + RegisterWindow` → `6) Behavior(registry)` → `7) Procedures.RunMessageLoop()`. `UiDefaults` debe ir primero porque `GetDefaultFont`/`MeasureContent` lo leen con DPI scaling. +> **Pipeline `Initialize()`:** `Application.Initialize()` ejecuta en orden: `1) UiDefaultsProvider.Set(new Win32UiDefaults())` → `2) InitCommonControlsEx(STANDARD_CLASSES)` → `3) ControlActivatorRegistry.EnsureInitialized()` → `4) new MasterRouter(registry)` → `5) foreach window: RegisterMain (CreateMainWindow + MaterializeContent + RegisterWindow + SetupSystemTrayIcon si hay `.SystemTray(...)`) o RegisterAlternative (rama `IMenu`: `MenuSurface` sin HWND + `EnsureMaterialized`; resto: CreateAlternativeWindow + MaterializeContent)` → `6) Behavior(registry + MainHwnd)` → `7) RaiseLaunched()` → `8) Procedures.RunMessageLoop()`. `UiDefaults` debe ir primero porque `GetDefaultFont`/`MeasureContent` lo leen con DPI scaling. # Flujo en **Resources** ```csharp @@ -117,7 +117,7 @@ WindowsApplication ``` ## Una ventana con un control personalizado -> Usa el 3er overload `IChildrenBuilder.View(Action)` para controles custom sin tipo genérico (`View sealed class` es para `T : IControl`; `IViewBuilder` es para contenido arbitrario con `Padding/Spacing/Children`). +> Usa el 3er overload `IChildrenBuilder.View(Action)` para controles custom sin tipo genérico (`View sealed class` es para `T : IViewSurface` — `IControl` para controles Win32 con HWND, `IMenu`/`IMenuItem` para superficies sin HWND; `IViewBuilder` es para contenido arbitrario con `Padding/Spacing/Children`). ```csharp WindowsApplication.Layout(ly => ly @@ -144,7 +144,42 @@ WindowsApplication.Layout(ly => ly .Initialize(); ``` -> `IChildrenBuilder` tiene 3 overloads: `View(Action>)` + `View(Func,View>)` (ambos con `View sealed class where T : class, IControl`) + `View(Action)` para este caso. +> `IChildrenBuilder` tiene 4 overloads: `View()` (superficie anónima, sin `Name` ni lookup `bh.*` — p. ej. separadores) + `View(Action>)` + `View(Func,View>)` (ambos con `View sealed class where T : class, IViewSurface`) + `View(Action)` para este caso. + +## Superficies de menú (`IMenu` / `IMenuItem`) +> `AlternativeWindow` acepta cualquier `IViewSurface`. Con `T = IMenu` no se crea HWND: `Application.RegisterAlternative` construye un `MenuSurface` (motor `HMENU` Win32 puro, `Core/Menus/Win32MenuEngine`) y lo registra por nombre. Los items se declaran con `View` (`Name` + `Text` + `IsEnabled` + `IsChecked` + `OnClick` + submenú vía `SubContent`); se materializan una vez (`EnsureMaterialized`) y se registran para `bh.*`. + +```csharp +WindowsApplication.Layout(ly => ly + .AlternativeWindow(m => m + .Name("MySystemTrayMenu") + .Content(c => c + .Children(ch => + { + ch.View(i => i.Name("Mi1").Text("Mostrar ventana principal")); + ch.View(i => i.Name("Mi2").Text("Salir")); + }) + ) + ) +) +.Initialize(); +``` + +## SystemTray en ventana (config-time) +> `.SystemTray(...)` vive en `IWindowConfig` y recibe `Action`: `Tooltip` (+ balloon con `TooltipShow`/`TooltipHide`) y suscripción de triggers. En `RegisterMain`, `SetupSystemTrayIcon` crea el broker (ventana oculta + `Shell_NotifyIconW` + `NOTIFYICON_VERSION_4`), aplica la configuración, añade el icono y registra la superficie como `"SystemTray"`. + +```csharp +WindowsApplication.Layout(ly => ly + .Window(iw => iw + .SystemTray(st => st.Tooltip("Mi app")) + .Name("MainWindow") + .Title("Easy Win App") + .Dimensions(420, 280) + .Content(...) + ) +) +.Initialize(); +``` # Flujo en **Behavior** ```csharp @@ -173,3 +208,50 @@ WindowsApplication ) .Initialize(); ``` + +## SystemTray y menús en **Behavior** +> El generator emite accessors tipados: `IControl` → `ControlAccess.Get`, `IBaseWindow` → `GetWindow`, `IViewSurface` (menús/items) → `GetSurface`. `bh.SystemTray` es miembro real de `IBehaviorBuilder` (nombre reservado, lazy). Triggers tipados sin nombres de dispositivo: `OnInputWithSpatialPosition` (+ overload con `TCount`: `OneTap…TenTap`) y `OnInputWithoutSpatialPosition`. `bh.WindowsApplication.OnLaunched` se dispara tras Behavior; `TaskbarButtonVisibility(bool)` usa `ITaskbarList`. + +```csharp +WindowsApplication + .Layout(ly => + { + ly.Window(iw => iw + .SystemTray(wst => wst.Tooltip("")) + .Name("MainWindow") + .Title("Easy Win App") + .Dimensions(420, 280) + .Position(WindowPositionOnScreen.Center) + .Content(c => c + .Children(ch => ch + .View(btn => btn.Name("BtnIncrement").Text("Click me")) + ) + ) + ); + ly.AlternativeWindow(m => m + .Name("MySystemTrayMenu") + .Content(c => c + .Children(ch => + { + ch.View(i => i.Name("Mi1").Text("Mostrar ventana principal")); + ch.View(i => i.Name("Mi2").Text("Salir")); + }) + ) + ); + }) + .Behavior(bh => + { + bh.WindowsApplication.OnLaunched(wa => wa.TaskbarButtonVisibility(false)); + bh.MainWindow.Visibility(false); + bh.SystemTray.Visibility(true); + + bh.SystemTray.OnInputWithSpatialPosition(() => bh.SystemTray.TooltipShow()); + bh.SystemTray.OnInputWithSpatialPosition(() => bh.MySystemTrayMenu.Show()); + bh.SystemTray.OnInputWithoutSpatialPosition(() => bh.MySystemTrayMenu.Show()); + bh.SystemTray.OnInputWithoutSpatialPosition>(() => bh.MySystemTrayMenu.Show()); + bh.SystemTray.OnInputWithSpatialPosition(() => { /* multi-tap */ }); + }) + .Initialize(); +``` + +> Mapeo OS→trigger (`SystemTrayBroker`): `WM_LBUTTONUP`/`NIN_SELECT`/`NIN_KEYSELECT`→`MainTap`, `WM_LBUTTONDBLCLK`→`MainDoubleTap`, `WM_RBUTTONUP`→`AlternativeTap1`, `WM_MBUTTONUP`→`AlternativeTap2`, `WM_MOUSEMOVE`/`NIN_POPUPOPEN`→`Hover` (flanco, reset 1s), `WM_CONTEXTMENU` de teclado→`KeyMenu` (+ `Chord` con Shift). `LongTap`/`Holding` son **derivados** (no nativos del OS): `WM_LBUTTONDOWN` + timer 500ms → `Holding` (aún presionado); soltar tras hold → `LongTap`; soltar antes → `MainTap`. Conteo encadenado con ventana `GetDoubleClickTime()` (cap 10). From 81dfa1bcaf80335eeea07b6272bf2d39884927f0 Mon Sep 17 00:00:00 2001 From: Raul Avila Catala Date: Sat, 19 Sep 2026 21:22:19 -0600 Subject: [PATCH 02/24] =?UTF-8?q?fix:=20callbacks=20tray=20empaquetados,?= =?UTF-8?q?=20tooltip=20cl=C3=A1sico=20y=20cat=C3=A1logo=20Culture=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Broker: máscara LOWORD + guard HIWORD (VERSION_4 empaqueta lParam), WM_CONTEXTMENU por callback con heurística de cursor, TaskbarList por vtable (adiós NotSupportedException de ComImport), higiene (callback!=0, AddIcon con throw). Tooltip clásico vía NIF_SHOWTIP; grupo ISystemTrayNotifications (IToolTipService + ITrayNotificationService); TooltipShow/Hide pasan a Notify/DismissNotification. MenuSurface se ancla al icono (fix HRESULT invertido). Culture automático (OS) + catálogo Cultures generado con global using static; Sample con preset pelado y OnClick en items. --- .../CulturesGenerator.cs | 106 ++++++++++++++++++ .../Common/ControlActivatorRegistry.cs | 2 +- .../Core/Application.cs | 22 +++- src/EasyWindowsApplication/Core/Constants.cs | 12 ++ .../Core/HandleRegistry.cs | 2 +- .../Core/MasterRouter.cs | 29 ++--- .../Core/Menus/MenuModel.cs | 4 +- .../Core/Menus/MenuSurface.cs | 9 +- .../Core/Menus/Win32MenuEngine.cs | 5 +- .../Core/ResourcesDictionaryImpl.cs | 4 +- .../Core/SystemTray/SystemTrayBroker.cs | 47 +++++++- .../Core/SystemTray/SystemTrayImpl.cs | 6 +- .../Core/TaskbarList.cs | 40 +++---- .../Core/Windowing/WindowImpl.cs | 10 +- .../Share/ISystemTray.cs | 5 +- .../Share/ISystemTrayNotifications.cs | 5 + .../Share/IToolTipService.cs | 6 + .../Share/ITrayNotificationService.cs | 7 ++ src/Sample/Program.cs | 54 +++++---- src/system_design.md | 23 +++- 20 files changed, 303 insertions(+), 95 deletions(-) create mode 100644 src/EasyWindowsApplication.Generators/CulturesGenerator.cs create mode 100644 src/EasyWindowsApplication/Share/ISystemTrayNotifications.cs create mode 100644 src/EasyWindowsApplication/Share/IToolTipService.cs create mode 100644 src/EasyWindowsApplication/Share/ITrayNotificationService.cs diff --git a/src/EasyWindowsApplication.Generators/CulturesGenerator.cs b/src/EasyWindowsApplication.Generators/CulturesGenerator.cs new file mode 100644 index 0000000..a5eca25 --- /dev/null +++ b/src/EasyWindowsApplication.Generators/CulturesGenerator.cs @@ -0,0 +1,106 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace EasyWindowsApplication.Generators; + +[Generator(LanguageNames.CSharp)] +public sealed class CulturesGenerator : IIncrementalGenerator +{ + private const string LibraryAssemblyName = "EasyWindowsApplication"; + + private static readonly string[,] Cultures = new[,] + { + { "En", "en" }, + { "EnUS", "en-US" }, + { "EnGB", "en-GB" }, + { "EnCA", "en-CA" }, + { "EnAU", "en-AU" }, + { "EnIN", "en-IN" }, + { "Es", "es" }, + { "EsES", "es-ES" }, + { "EsMX", "es-MX" }, + { "EsAR", "es-AR" }, + { "EsCO", "es-CO" }, + { "EsCL", "es-CL" }, + { "Fr", "fr" }, + { "FrFR", "fr-FR" }, + { "FrCA", "fr-CA" }, + { "De", "de" }, + { "DeDE", "de-DE" }, + { "DeAT", "de-AT" }, + { "DeCH", "de-CH" }, + { "It", "it" }, + { "ItIT", "it-IT" }, + { "ItCH", "it-CH" }, + { "Pt", "pt" }, + { "PtBR", "pt-BR" }, + { "PtPT", "pt-PT" }, + { "Nl", "nl" }, + { "NlNL", "nl-NL" }, + { "NlBE", "nl-BE" }, + { "Pl", "pl" }, + { "PlPL", "pl-PL" }, + { "Ru", "ru" }, + { "RuRU", "ru-RU" }, + { "UkUA", "uk-UA" }, + { "TrTR", "tr-TR" }, + { "SvSE", "sv-SE" }, + { "NbNO", "nb-NO" }, + { "DaDK", "da-DK" }, + { "FiFI", "fi-FI" }, + { "CsCZ", "cs-CZ" }, + { "HuHU", "hu-HU" }, + { "ElGR", "el-GR" }, + { "HeIL", "he-IL" }, + { "ThTH", "th-TH" }, + { "ViVN", "vi-VN" }, + { "IdID", "id-ID" }, + { "MsMY", "ms-MY" }, + { "RoRO", "ro-RO" }, + { "BgBG", "bg-BG" }, + { "HrHR", "hr-HR" }, + { "SkSK", "sk-SK" }, + { "CaES", "ca-ES" }, + { "Zh", "zh" }, + { "ZhCN", "zh-CN" }, + { "ZhTW", "zh-TW" }, + { "ZhHK", "zh-HK" }, + { "Ja", "ja" }, + { "JaJP", "ja-JP" }, + { "KoKR", "ko-KR" }, + { "HiIN", "hi-IN" }, + { "Ar", "ar" }, + { "ArSA", "ar-SA" }, + { "ArEG", "ar-EG" }, + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput( + context.CompilationProvider.Select(static (compilation, _) => + compilation.AssemblyName == LibraryAssemblyName), + static (spc, isLibrary) => + { + if (isLibrary) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("using System.Globalization;"); + sb.AppendLine(); + sb.AppendLine("namespace EasyWindowsApplication.Share;"); + sb.AppendLine(); + sb.AppendLine("public static class Cultures"); + sb.AppendLine("{"); + for (int i = 0; i < Cultures.GetLength(0); i++) + { + sb.AppendLine($" public static readonly CultureInfo CultureInfo{Cultures[i, 0]} = CultureInfo.GetCultureInfo(\"{Cultures[i, 1]}\");"); + } + sb.AppendLine("}"); + spc.AddSource("Cultures.g.cs", sb.ToString()); + } + + spc.AddSource("CulturesUsings.g.cs", + "// \nglobal using static EasyWindowsApplication.Share.Cultures;\n"); + }); + } +} diff --git a/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs b/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs index 66aaf63..7f9f8b6 100644 --- a/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs +++ b/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs @@ -73,7 +73,7 @@ internal bool TryGetFactoryForControl(IControl control, out INativeHandleFactory #pragma warning restore IL2075 // fallback por base types var bt = t.BaseType; - while (bt != null && bt != typeof(object)) + while (bt is not null && bt != typeof(object)) { if (_handleFactories.TryGetValue(bt, out factory)) return true; diff --git a/src/EasyWindowsApplication/Core/Application.cs b/src/EasyWindowsApplication/Core/Application.cs index e33e9a6..43abedc 100644 --- a/src/EasyWindowsApplication/Core/Application.cs +++ b/src/EasyWindowsApplication/Core/Application.cs @@ -2,7 +2,6 @@ using EasyWindowsApplication.Common; using EasyWindowsApplication.Share; using EasyWindowsApplication.Core.LayoutEngine; -using EasyWindowsApplication.Core.Windowing; using EasyWindowsApplication.Share.Infrastructure; namespace EasyWindowsApplication.Core; @@ -15,13 +14,13 @@ internal sealed class Application : internal ResourcesDictionaryImpl ResourcesDictionary { get; } = new(); internal BehaviorBuilderImpl BehaviorBuilder { get; } = new(); - private readonly List _windows = new(); + private readonly List _windows = []; private MasterRouter _router = null!; private readonly HandleRegistry _registry = new(); private Action? _pendingBehavior; private nint _mainHwnd; private SystemTray.SystemTrayBroker? _trayBroker; - private readonly List _pendingMenuSurfaces = new(); + private readonly List _pendingMenuSurfaces = []; public IApplicationLayoutPhase Resources(Action configure) { @@ -58,6 +57,16 @@ public void Initialize() // 1. Registrar defaults PRIMERO (antes de cualquier control o HFONT) try { UiDefaultsProvider.Set(new Win32ControlsModule.Backend.Win32UiDefaults()); } catch { } + // Cultura explícita (Resources) o automática del OS (no tocar nada) + var culture = ResourcesDictionary.SettingsContext.SelectedCulture; + if (culture is not null) + { + System.Globalization.CultureInfo.CurrentCulture = culture; + System.Globalization.CultureInfo.CurrentUICulture = culture; + System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture; + System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture; + } + var icc = new INITCOMMONCONTROLSEX { dwSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), @@ -126,7 +135,10 @@ private void SetupSystemTrayIcon(Action configure, nint mainHwnd) tray = new SystemTray.SystemTrayImpl(broker); configure(tray); nint hIcon = Core.Windowing.Procedures.LoadAppIcon(); - broker.AddIcon(hIcon); + if (hIcon == 0) + throw new InvalidOperationException("No hay icono disponible para el SystemTray."); + if (!broker.AddIcon(hIcon)) + throw new InvalidOperationException("Shell_NotifyIconW(NIM_ADD) falló para el SystemTray."); _registry.RegisterSurface(tray.Name, tray); _trayBroker = broker; WireMenuSurfaces(); @@ -168,7 +180,7 @@ private void RegisterAlternative(WindowModel window) _registry.RegisterWindow(win); // Fase 2: materializar contenido para AlternativeWindow también win.MaterializeContent(window, _registry, _router); - if (window.Position == WindowPositionOnScreen.Center) + if (window.Position is WindowPositionOnScreen.Center) { // Center para AlternativeWindow (pre-calcula posición pero no muestra) nint monitor = Win32.MonitorFromWindow(hwnd, MONITOR.DEFAULTTONEAREST); diff --git a/src/EasyWindowsApplication/Core/Constants.cs b/src/EasyWindowsApplication/Core/Constants.cs index 7019af8..faf64a0 100644 --- a/src/EasyWindowsApplication/Core/Constants.cs +++ b/src/EasyWindowsApplication/Core/Constants.cs @@ -272,6 +272,18 @@ internal static class NIF internal const uint SHOWTIP = 0x00000080; } + internal static class NIIF + { + internal const uint NONE = 0x00000000; + internal const uint INFO = 0x00000001; + internal const uint WARNING = 0x00000002; + internal const uint ERROR = 0x00000003; + internal const uint USER = 0x00000004; + internal const uint NOSOUND = 0x00000010; + internal const uint LARGE_ICON = 0x00000020; + internal const uint RESPECT_QUIET_TIME = 0x00000080; + } + internal static class NIN { internal const uint SELECT = 0x0400; diff --git a/src/EasyWindowsApplication/Core/HandleRegistry.cs b/src/EasyWindowsApplication/Core/HandleRegistry.cs index a85bcc5..6995c2e 100644 --- a/src/EasyWindowsApplication/Core/HandleRegistry.cs +++ b/src/EasyWindowsApplication/Core/HandleRegistry.cs @@ -106,7 +106,7 @@ internal void UnregisterWindowByHwnd(nint hwnd) string? keyToRemove = null; foreach (var kv in _windowByName) if (kv.Value.Hwnd == hwnd) { keyToRemove = kv.Key; break; } - if (keyToRemove != null) _windowByName.Remove(keyToRemove); + if (keyToRemove is not null) _windowByName.Remove(keyToRemove); } internal IBaseWindow? GetWindowByHwnd(nint hwnd) diff --git a/src/EasyWindowsApplication/Core/MasterRouter.cs b/src/EasyWindowsApplication/Core/MasterRouter.cs index 9ff73dc..3c1683a 100644 --- a/src/EasyWindowsApplication/Core/MasterRouter.cs +++ b/src/EasyWindowsApplication/Core/MasterRouter.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using EasyWindowsApplication.Common; using EasyWindowsApplication.Core.Windowing; @@ -9,15 +10,15 @@ namespace EasyWindowsApplication.Core; internal sealed class MasterRouter { - private readonly Dictionary<(nint Hwnd, uint Msg), Win32MessageHandler> _handlers = new(); + private readonly Dictionary<(nint Hwnd, uint Msg), Win32MessageHandler> _handlers = []; private readonly HandleRegistry _registry; private nint _mainHwnd; private bool _isResizing; - private readonly Dictionary _windowBrushes = new(); - private readonly Dictionary _controlBrushes = new(); - private readonly Dictionary> _layoutGroupBackgrounds = new(); - private readonly Dictionary _scrollOffsets = new(); + private readonly Dictionary _windowBrushes = []; + private readonly Dictionary _controlBrushes = []; + private readonly Dictionary> _layoutGroupBackgrounds = []; + private readonly Dictionary _scrollOffsets = []; internal MasterRouter(HandleRegistry registry) { @@ -30,7 +31,7 @@ internal void AddLayoutGroupBackground(nint hwnd, RECT rect, Color color) int colorRef = color.ToCOLORREF(); if (!_layoutGroupBackgrounds.TryGetValue(hwnd, out var list)) { - list = new List<(RECT, int)>(); + list = []; _layoutGroupBackgrounds[hwnd] = list; } list.Add((rect, colorRef)); @@ -79,7 +80,7 @@ internal static nint WndProcTrampoline(nint hwnd, uint msg, nint wParam, nint lP } catch (Exception ex) { - System.Diagnostics.Debug.Fail(ex.ToString()); + Debug.Fail(ex.ToString()); return Win32.DefWindowProcW(hwnd, msg, wParam, lParam); } } @@ -90,7 +91,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) if (msg == WM.CLOSE) { var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { bool cancel = false; if (win is WindowImpl wi) cancel = wi.RaiseClosing(); @@ -107,7 +108,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) { _isResizing = false; var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { Win32.GetClientRect(hwnd, out RECT rc); int w = rc.Right - rc.Left; @@ -118,7 +119,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) else if (msg == WM.SIZE) { var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { int wParamType = (int)wParam; if (wParamType != WMSIZE.MINIMIZED) @@ -139,7 +140,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) else if (msg == WM.MOVE) { var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { int x = (int)(short)Win32Helpers.LOWORD(lParam); int y = (int)(short)Win32Helpers.HIWORD(lParam); @@ -149,7 +150,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) else if (msg == WM.ACTIVATE) { var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { int state = (int)Win32Helpers.LOWORD(wParam); if (state == WA.INACTIVE) @@ -231,7 +232,7 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) { // Disparar Closed antes de limpieza var win = _registry.GetWindowByHwnd(hwnd); - if (win != null) + if (win is not null) { if (win is WindowImpl wi) wi.RaiseClosed(); else if (win is AlternativeWindowImpl aw) aw.RaiseClosed(); diff --git a/src/EasyWindowsApplication/Core/Menus/MenuModel.cs b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs index 8d6c7ed..62e1805 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuModel.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs @@ -7,13 +7,13 @@ internal sealed class MenuItemModel internal bool IsEnabled = true; internal bool IsChecked; internal bool IsSeparator; - internal List Children = new(); + internal List Children = []; internal Action? OnClick; } internal sealed class MenuModel { - internal List Items = new(); + internal List Items = []; internal MenuItemModel? FindById(int id) { diff --git a/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs index 5700bf0..fac5986 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.InteropServices; using EasyWindowsApplication.Share; @@ -27,8 +28,11 @@ internal void SetIconAnchor(nint trayHwnd, uint trayId) uID = trayId, guidItem = Guid.Empty }; - if (Win32.Shell_NotifyIconGetRect(ref identifier, out var rect) != 0) + // S_OK (0) = rect válido: el menú se ancla al icono (foco), nunca al mouse. + // Sin rect (icono no localizable) el cursor es fallback documentado. + if (Win32.Shell_NotifyIconGetRect(ref identifier, out var rect) == 0) return (rect.Right, rect.Top); + Win32.GetCursorPos(out var pt); return (pt.X, pt.Y); }; @@ -102,8 +106,7 @@ internal MenuModel BuildModel() internal MenuModel EnsureMaterialized() { - if (_cachedModel is null) - _cachedModel = BuildModel(); + _cachedModel ??= BuildModel(); return _cachedModel; } diff --git a/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs index e877a55..4e59167 100644 --- a/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs +++ b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + namespace EasyWindowsApplication.Core.Menus; internal static class Win32MenuEngine @@ -44,7 +47,7 @@ internal static int Show(nint hwndOwner, MenuModel model, int x, int y, uint ali return id; } finally - { + { Win32.DestroyMenu(hMenu); } } diff --git a/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs b/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs index 6914c4f..adabac8 100644 --- a/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs +++ b/src/EasyWindowsApplication/Core/ResourcesDictionaryImpl.cs @@ -24,6 +24,7 @@ public IResourcesDictionary Services(Action configure) internal sealed class SettingsBuilderImpl : ISettingsBuilder { internal AppConfigFileConfig AppConfig { get; } = new(); + internal CultureInfo? SelectedCulture { get; private set; } public ISettingsBuilder UseWinApi() { @@ -38,7 +39,8 @@ public ISettingsBuilder AppConfigFile(Action configure) public ISettingsBuilder Culture(CultureInfo cultureInfo) { - throw new NotImplementedException(); + SelectedCulture = cultureInfo; + return this; } } diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs index db73b03..d0aace8 100644 --- a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using EasyWindowsApplication.Core.Windowing; using EasyWindowsApplication.Share.Input; namespace EasyWindowsApplication.Core.SystemTray; @@ -46,6 +45,8 @@ internal static SystemTrayBroker Create(MasterRouter router, Action d { nint hwnd = Windowing.Procedures.CreateHiddenWindow(router); uint callback = Win32.RegisterWindowMessageW("EasyWindowsApplication.Tray.v1"); + if (callback == 0) + throw new InvalidOperationException("RegisterWindowMessageW failed for tray callback."); var broker = new SystemTrayBroker(hwnd, callback, dispatch); router.RegisterHandler(hwnd, callback, broker.OnCallback); router.RegisterHandler(hwnd, WM.CONTEXTMENU, broker.OnContextMenu); @@ -57,7 +58,7 @@ internal static SystemTrayBroker Create(MasterRouter router, Action d internal bool AddIcon(nint icon) { _icon = icon; - var data = NewData(NIF.MESSAGE | NIF.ICON | NIF.TIP); + var data = NewData(NIF.MESSAGE | NIF.ICON | NIF.TIP | NIF.SHOWTIP); CopyStrings(ref data, _tooltip, null, null); if (!Win32.Shell_NotifyIconW(NIM.ADD, ref data)) return false; _iconAdded = true; @@ -78,7 +79,7 @@ internal void UpdateTooltip(string tooltip) { _tooltip = tooltip; if (!_iconAdded) return; - var data = NewData(NIF.TIP); + var data = NewData(NIF.TIP | NIF.SHOWTIP); CopyStrings(ref data, tooltip, null, null); Win32.Shell_NotifyIconW(NIM.MODIFY, ref data); } @@ -123,14 +124,18 @@ internal void SetVisible(bool visible) private nint OnCallback(nint wParam, nint lParam) { - switch ((uint)lParam) + // VERSION_4 empaqueta lParam = mensaje (LOWORD) | uID del icono (HIWORD). + uint packed = (uint)lParam; + if ((packed >> 16) != IconId) return 0; + switch (packed & 0xFFFF) { case WM_MOUSEMOVE: OnHover(); break; case WM_LBUTTONDOWN: OnButtonDown(); break; case WM_LBUTTONUP: OnButtonUp(); break; case WM_LBUTTONDBLCLK: Fire(typeof(MainDoubleTap)); break; - case WM_RBUTTONUP: Fire(typeof(AlternativeTap1)); break; - case WM_MBUTTONUP: Fire(typeof(AlternativeTap2)); break; + case WM_RBUTTONUP: if (IsCursorOverIcon()) Fire(typeof(AlternativeTap1)); break; + case WM_MBUTTONUP: if (IsCursorOverIcon()) Fire(typeof(AlternativeTap2)); break; + case WM.CONTEXTMENU: OnContextMenuKey(); break; case var m when m == NIN.SELECT || m == NIN.KEYSELECT: Fire(typeof(MainTap)); break; case var m when m == NIN.POPUPOPEN: OnHover(); break; } @@ -146,6 +151,36 @@ private nint OnContextMenu(nint wParam, nint lParam) return 0; } + private void OnContextMenuKey() + { + // WM_CONTEXTMENU por callback llega igual con ratón y con teclado. + // Heurística: el click de ratón exige el cursor sobre el icono; + // la tecla de menú funciona con el cursor en cualquier parte. + if (IsCursorOverIcon()) + { + Fire(typeof(AlternativeTap1)); + return; + } + Fire(typeof(KeyMenu)); + if (Win32.GetKeyState(VK.SHIFT) < 0) + _dispatch(typeof(Chord), 1); + } + + private bool IsCursorOverIcon() + { + if (!Win32.GetCursorPos(out var pt)) return false; + var identifier = new NOTIFYICONIDENTIFIER + { + cbSize = (uint)Marshal.SizeOf(), + hWnd = Hwnd, + uID = IconId, + guidItem = Guid.Empty + }; + if (Win32.Shell_NotifyIconGetRect(ref identifier, out var rect) != 0) + return false; + return pt.X >= rect.Left && pt.X < rect.Right && pt.Y >= rect.Top && pt.Y < rect.Bottom; + } + private nint OnTimer(nint wParam, nint lParam) { if ((nuint)wParam != HoldTimerId) return 0; diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs index b5765e8..d2280ee 100644 --- a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs @@ -47,13 +47,13 @@ public ISystemTray Tooltip(string text) return this; } - public ISystemTray TooltipShow() + public ISystemTray Notify(string title, string message) { - _broker.ShowBalloon(Name, _tooltip); + _broker.ShowBalloon(title, message); return this; } - public ISystemTray TooltipHide() + public ISystemTray DismissNotification() { _broker.HideBalloon(); return this; diff --git a/src/EasyWindowsApplication/Core/TaskbarList.cs b/src/EasyWindowsApplication/Core/TaskbarList.cs index ddc38b4..7a0d873 100644 --- a/src/EasyWindowsApplication/Core/TaskbarList.cs +++ b/src/EasyWindowsApplication/Core/TaskbarList.cs @@ -4,23 +4,15 @@ namespace EasyWindowsApplication.Core; internal static partial class TaskbarList { - [ComImport] - [Guid("56FDF344-FD6D-11D0-958A-006097C9A090")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface ITaskbarList - { - void HrInit(); - void AddTab(nint hwnd); - void DeleteTab(nint hwnd); - void ActivateTab(nint hwnd); - void SetActiveAlt(nint hwnd); - } - [LibraryImport("ole32.dll")] private static partial int CoCreateInstance( ref Guid rclsid, nint pUnkOuter, uint dwClsContext, ref Guid riid, out nint ppv); - internal static bool SetVisible(nint hwnd, bool visible) + // ITaskbarList vtable: 0 QueryInterface, 1 AddRef, 2 Release, + // 3 HrInit, 4 AddTab, 5 DeleteTab, 6 ActivateTab, 7 SetActiveAlt. + // Llamada manual (sin ComImport: el dispatch clásico lanza + // NotSupportedException en .NET moderno). AOT-safe. + internal static unsafe bool SetVisible(nint hwnd, bool visible) { if (hwnd == 0) return false; try @@ -31,22 +23,18 @@ internal static bool SetVisible(nint hwnd, bool visible) if (hr != 0 || ppv == 0) return false; try { - var list = (ITaskbarList)Marshal.GetObjectForIUnknown(ppv); - try - { - list.HrInit(); - if (visible) list.AddTab(hwnd); - else list.DeleteTab(hwnd); - return true; - } - finally - { - Marshal.ReleaseComObject(list); - } + nint* vtable = *(nint**)ppv; + var hrInit = (delegate* unmanaged[Stdcall])vtable[3]; + var addTab = (delegate* unmanaged[Stdcall])vtable[4]; + var deleteTab = (delegate* unmanaged[Stdcall])vtable[5]; + if (hrInit(ppv) != 0) return false; + int result = visible ? addTab(ppv, hwnd) : deleteTab(ppv, hwnd); + return result == 0; } finally { - Marshal.Release(ppv); + var release = (delegate* unmanaged[Stdcall])((*(nint**)ppv)[2]); + release(ppv); } } catch diff --git a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs index ef4630c..1b75727 100644 --- a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs @@ -150,7 +150,7 @@ public void ScrollTo(int x, int y) _scrollY = newY; // Actualizar SCROLLINFO - if (_scrollConfig != null) + if (_scrollConfig is not null) { if (_maxScrollY > 0 || _scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) { @@ -286,7 +286,7 @@ private void ConfigureScrollbars() // Calcular tamaños totales del contenido int totalH = 0; int totalW = 0; - if (_materializedChildren != null && _materializedChildren.Count > 0) + if (_materializedChildren is not null && _materializedChildren.Count > 0) { // Altura total para VerticalStack foreach (var c in _materializedChildren) @@ -379,14 +379,14 @@ internal void RaiseResized(int w, int h) _clientW = w; _clientH = h; - if (_materializedChildren != null && _contentModel != null && _materializedChildren.Count > 0) + if (_materializedChildren is not null && _contentModel is not null && _materializedChildren.Count > 0) { var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); engine.Execute(_materializedChildren, w, h, _contentModel.Spacing, _contentModel.Padding); } // Reconfigurar scrollbars tras resize (puede cambiar necesidad de scroll) - if (_scrollConfig != null) ConfigureScrollbars(); + if (_scrollConfig is not null) ConfigureScrollbars(); Resized?.Invoke(this, new WindowResizedEventArgs(w, h)); } @@ -418,7 +418,7 @@ internal void MaterializeContent(WindowModel window, HandleRegistry registry, Ma _clientH = (int)availH; // Fase 4: Scroll - if (window.ScrollConfig != null) + if (window.ScrollConfig is not null) { _scrollConfig = window.ScrollConfig; _scrollX = (int)window.ScrollConfig.ScrollX; diff --git a/src/EasyWindowsApplication/Share/ISystemTray.cs b/src/EasyWindowsApplication/Share/ISystemTray.cs index b7da260..e0469c5 100644 --- a/src/EasyWindowsApplication/Share/ISystemTray.cs +++ b/src/EasyWindowsApplication/Share/ISystemTray.cs @@ -2,11 +2,8 @@ namespace EasyWindowsApplication.Share; -public interface ISystemTray : IViewSurface +public interface ISystemTray : IViewSurface, ISystemTrayNotifications { - ISystemTray Tooltip(string text); - ISystemTray TooltipShow(); - ISystemTray TooltipHide(); ISystemTray OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger; ISystemTray OnInputWithSpatialPosition(Action handler) diff --git a/src/EasyWindowsApplication/Share/ISystemTrayNotifications.cs b/src/EasyWindowsApplication/Share/ISystemTrayNotifications.cs new file mode 100644 index 0000000..458a439 --- /dev/null +++ b/src/EasyWindowsApplication/Share/ISystemTrayNotifications.cs @@ -0,0 +1,5 @@ +namespace EasyWindowsApplication.Share; + +public interface ISystemTrayNotifications : IToolTipService, ITrayNotificationService +{ +} diff --git a/src/EasyWindowsApplication/Share/IToolTipService.cs b/src/EasyWindowsApplication/Share/IToolTipService.cs new file mode 100644 index 0000000..fb0a764 --- /dev/null +++ b/src/EasyWindowsApplication/Share/IToolTipService.cs @@ -0,0 +1,6 @@ +namespace EasyWindowsApplication.Share; + +public interface IToolTipService +{ + ISystemTray Tooltip(string text); +} diff --git a/src/EasyWindowsApplication/Share/ITrayNotificationService.cs b/src/EasyWindowsApplication/Share/ITrayNotificationService.cs new file mode 100644 index 0000000..dcbbb07 --- /dev/null +++ b/src/EasyWindowsApplication/Share/ITrayNotificationService.cs @@ -0,0 +1,7 @@ +namespace EasyWindowsApplication.Share; + +public interface ITrayNotificationService +{ + ISystemTray Notify(string title, string message); + ISystemTray DismissNotification(); +} diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index bb749c0..8d60b60 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -6,30 +6,31 @@ //WindowsApplication.Layout(ly => ly.Window()).Initialize(); WindowsApplication - .Resources(rd => rd.Setting(st => { + .Resources(rd => rd.Setting(st => + { st.UseWinApi(); - st.Culture(new System.Globalization.CultureInfo("en-us")); + st.Culture(CultureInfoEnUS); })) .Layout(ly => { ly.Window(iw => iw - .SystemTray(st => - { - st.Tooltip(""); - }) - .Name("MainWindow") - .Title("Easy Win App") - .Dimensions(420, 280) - .Position(WindowPositionOnScreen.Center) - .Content(c => c - .Children(ch => ch - .View(btn => btn - .Name("BtnIncrement") - .Text("Click me") - ) + .SystemTray(st => + { + st.Tooltip("Click here for show main window"); + }) + .Name("MainWindow") + .Title("Easy Win App") + .Dimensions(420, 280) + .Position(WindowPositionOnScreen.Center) + .Content(c => c + .Children(ch => ch + .View(btn => btn + .Name("BtnIncrement") + .Text("Click me") ) ) - ); + ) + ); ly.AlternativeWindow(m => m .Name("MySystemTrayMenu") .Content(c => c @@ -50,9 +51,24 @@ bh.MainWindow.Visibility(false); bh.SystemTray.Visibility(true); }); - bh.SystemTray.OnInputWithSpatialPosition(() => { bh.SystemTray.TooltipShow(); }); - bh.SystemTray.OnInputWithSpatialPosition(() => { bh.MySystemTrayMenu.Show(); }); + bh.SystemTray.OnInputWithSpatialPosition(() => { bh.MySystemTrayMenu.Show(); }); + bh.SystemTray.OnInputWithSpatialPosition(() => + { + bh.SystemTray.Visibility(false); + bh.WindowsApplication.TaskbarButtonVisibility(true); + bh.MainWindow.Visibility(true); + }); bh.SystemTray.OnInputWithoutSpatialPosition(() => { bh.MySystemTrayMenu.Show(); }); bh.SystemTray.OnInputWithoutSpatialPosition>(() => { bh.MySystemTrayMenu.Show(); }); + bh.Mi1.OnClick(() => + { + bh.SystemTray.Visibility(false); + bh.WindowsApplication.TaskbarButtonVisibility(true); + bh.MainWindow.Visibility(true); + }); + bh.Mi2.OnClick(() => + { + bh.MainWindow.Close(); + }); }) .Initialize(); diff --git a/src/system_design.md b/src/system_design.md index 6e4e329..771acbe 100644 --- a/src/system_design.md +++ b/src/system_design.md @@ -61,6 +61,20 @@ WindowsApplication > **Nota `UseWinApi()` vs `UiDefaults`:** `UseWinApi()` solo afecta al **Source Generator** (`EAWIN002`). En runtime es `SettingsBuilderImpl.UseWinApi() => this` (no-op). El mecanismo runtime real es `Core/UiDefaults`: `Application.Initialize()` llama `UiDefaultsProvider.Set(new Win32UiDefaults())` antes de `InitCommonControlsEx` y de crear HWNDs. `ControlProcedures.GetDefaultFont()` y `Button.MeasureContent()` leen `PreferredHeight`/`FontSpec` vía `UiDefaultsProvider.Current` con DPI scaling `96→dpiActual` (ver `CONTRIBUTING.md` § Arquitectura). +## Cultura (`Culture`) — explícita o automática del OS +> Cadena de prioridad: 1) `st.Culture(...)` explícito → gana; 2) sin especificar → automático = locale del OS (el framework no toca nada); 3) `.csproj` sin efecto (`` solo es metadatos para `ResourceManager`; `` fuerza invariante y se respeta). Si hay cultura explícita, `Initialize` fija las 4 (`CurrentCulture` + `CurrentUICulture` del hilo actual y `DefaultThreadCurrentCulture` + `DefaultThreadCurrentUICulture` para hilos futuros). +> Sin strings libres: lo único válido sale del catálogo `Cultures` (generado, ~50 curadas, instancia cacheada vía `GetCultureInfo`). El generator emite además `global using static` → los presets se usan pelados (`CultureInfoEnUS`) con IntelliSense `CultureInfo*`. + +```csharp +WindowsApplication + .Resources(rd => rd.Setting(st => st + .UseWinApi() + .Culture(CultureInfoEnUS) // o .Culture(Cultures.CultureInfoJaJP); sin llamada = OS + )) + .Layout(...) + .Initialize(); +``` + # Flujo en **Layout** ## Una ventana sin componentes ```csharp @@ -147,7 +161,7 @@ WindowsApplication.Layout(ly => ly > `IChildrenBuilder` tiene 4 overloads: `View()` (superficie anónima, sin `Name` ni lookup `bh.*` — p. ej. separadores) + `View(Action>)` + `View(Func,View>)` (ambos con `View sealed class where T : class, IViewSurface`) + `View(Action)` para este caso. ## Superficies de menú (`IMenu` / `IMenuItem`) -> `AlternativeWindow` acepta cualquier `IViewSurface`. Con `T = IMenu` no se crea HWND: `Application.RegisterAlternative` construye un `MenuSurface` (motor `HMENU` Win32 puro, `Core/Menus/Win32MenuEngine`) y lo registra por nombre. Los items se declaran con `View` (`Name` + `Text` + `IsEnabled` + `IsChecked` + `OnClick` + submenú vía `SubContent`); se materializan una vez (`EnsureMaterialized`) y se registran para `bh.*`. +> `AlternativeWindow` acepta cualquier `IViewSurface`. Con `T = IMenu` no se crea HWND: `Application.RegisterAlternative` construye un `MenuSurface` (motor `HMENU` Win32 puro, `Core/Menus/Win32MenuEngine`) y lo registra por nombre. Los items se declaran con `View` (`Name` + `Text` + `IsEnabled` + `IsChecked` + `OnClick` + submenú vía `SubContent`); se materializan una vez (`EnsureMaterialized`) y se registran para `bh.*`. `Show()` se ancla al icono (`Shell_NotifyIconGetRect`): los triggers sin posición espacial nunca consumen la posición del mouse (el cursor es solo fallback si no hay rect). ```csharp WindowsApplication.Layout(ly => ly @@ -166,7 +180,7 @@ WindowsApplication.Layout(ly => ly ``` ## SystemTray en ventana (config-time) -> `.SystemTray(...)` vive en `IWindowConfig` y recibe `Action`: `Tooltip` (+ balloon con `TooltipShow`/`TooltipHide`) y suscripción de triggers. En `RegisterMain`, `SetupSystemTrayIcon` crea el broker (ventana oculta + `Shell_NotifyIconW` + `NOTIFYICON_VERSION_4`), aplica la configuración, añade el icono y registra la superficie como `"SystemTray"`. +> `.SystemTray(...)` vive en `IWindowConfig` y recibe `Action`: `Tooltip` (hint clásico, automático al hover vía `NIF_TIP` + `NIF_SHOWTIP`) y suscripción de triggers. Notificaciones reales aparte: `Notify(title, message)` / `DismissNotification()` (toast con banner + sonido, espejo de `AppNotification`). En `RegisterMain`, `SetupSystemTrayIcon` crea el broker (ventana oculta + `Shell_NotifyIconW` + `NOTIFYICON_VERSION_4`), aplica la configuración, añade el icono y registra la superficie como `"SystemTray"`. ```csharp WindowsApplication.Layout(ly => ly @@ -245,7 +259,6 @@ WindowsApplication bh.MainWindow.Visibility(false); bh.SystemTray.Visibility(true); - bh.SystemTray.OnInputWithSpatialPosition(() => bh.SystemTray.TooltipShow()); bh.SystemTray.OnInputWithSpatialPosition(() => bh.MySystemTrayMenu.Show()); bh.SystemTray.OnInputWithoutSpatialPosition(() => bh.MySystemTrayMenu.Show()); bh.SystemTray.OnInputWithoutSpatialPosition>(() => bh.MySystemTrayMenu.Show()); @@ -254,4 +267,6 @@ WindowsApplication .Initialize(); ``` -> Mapeo OS→trigger (`SystemTrayBroker`): `WM_LBUTTONUP`/`NIN_SELECT`/`NIN_KEYSELECT`→`MainTap`, `WM_LBUTTONDBLCLK`→`MainDoubleTap`, `WM_RBUTTONUP`→`AlternativeTap1`, `WM_MBUTTONUP`→`AlternativeTap2`, `WM_MOUSEMOVE`/`NIN_POPUPOPEN`→`Hover` (flanco, reset 1s), `WM_CONTEXTMENU` de teclado→`KeyMenu` (+ `Chord` con Shift). `LongTap`/`Holding` son **derivados** (no nativos del OS): `WM_LBUTTONDOWN` + timer 500ms → `Holding` (aún presionado); soltar tras hold → `LongTap`; soltar antes → `MainTap`. Conteo encadenado con ventana `GetDoubleClickTime()` (cap 10). +> Mapeo OS→trigger (`SystemTrayBroker`, `lParam` empaquetado v4: `LOWORD` = mensaje, `HIWORD` = uID): `WM_LBUTTONUP`/`NIN_SELECT`/`NIN_KEYSELECT`→`MainTap`, `WM_LBUTTONDBLCLK`→`MainDoubleTap`, `WM_RBUTTONUP`→`AlternativeTap1` y `WM_MBUTTONUP`→`AlternativeTap2` (ambos solo con cursor sobre el icono), `WM_MOUSEMOVE`/`NIN_POPUPOPEN`→`Hover` (flanco, reset 1s), `WM_CONTEXTMENU` por callback→`AlternativeTap1` (cursor encima) o `KeyMenu` (+ `Chord` con Shift, cursor fuera = teclado). `LongTap`/`Holding` son **derivados** (no nativos del OS): `WM_LBUTTONDOWN` + timer 500ms → `Holding` (aún presionado); soltar tras hold → `LongTap`; soltar antes → `MainTap`. Conteo encadenado con ventana `GetDoubleClickTime()` (cap 10). +> +> Tooltip vs notificación (`ISystemTrayNotifications` = `IToolTipService` + `ITrayNotificationService`, punto de extensión para módulos): `Tooltip(text)` = hint clásico (`NIF_TIP` + `NIF_SHOWTIP`, automático al hover, silencioso); `Notify(title, message)` / `DismissNotification()` = toast real (banner + sonido + Action Center, espejo de `AppNotification`). From 19ccb6deafe1bc8740977945a38dda6cbd6a322e Mon Sep 17 00:00:00 2001 From: Raul Avila Catala Date: Mon, 21 Sep 2026 20:58:50 -0600 Subject: [PATCH 03/24] refactor: consolidar entrada de usuario en IInputSurface compartido (SurfaceInputHub) (#48) --- CONTRIBUTING.md | 6 +- README.md | 2 +- .../EasyBehaviorGenerator.cs | 4 +- .../Core/ControlBase.cs | 34 +++--- .../Core/MasterRouter.cs | 9 +- .../Core/Menus/MenuItem.cs | 18 ++- .../Core/Menus/MenuModel.cs | 2 +- .../Core/Menus/MenuSeparatorItem.cs | 6 - .../Core/Menus/MenuSurface.cs | 14 +-- .../Core/Menus/Win32MenuEngine.cs | 2 +- .../Core/Surfaces/IInputFeed.cs | 10 ++ .../Core/Surfaces/SurfaceInputHub.cs | 65 +++++++++++ .../Core/Surfaces/TapChainCounter.cs | 42 +++++++ .../Core/SystemTray/SystemTrayBroker.cs | 34 +----- .../Core/SystemTray/SystemTrayImpl.cs | 61 ++-------- .../Core/Windowing/AlternativeWindowImpl.cs | 42 ++++--- .../Core/Windowing/WindowImpl.cs | 105 ++++++++++-------- .../Share/IAlternativeWindow.cs | 2 +- .../Share/IInputSurface.cs | 20 ++++ src/EasyWindowsApplication/Share/IMenuItem.cs | 2 +- .../Share/IMenuItemSeparator.cs | 4 +- .../Share/ISystemTray.cs | 12 +- .../Share/IViewSurface.cs | 3 - src/EasyWindowsApplication/Share/IWindow.cs | 2 +- .../Share/Input/ISpatialPositionTrigger.cs | 2 - .../Share/Input/SurfaceInputExtensions.cs | 29 +++++ src/EasyWindowsApplication/Share/View.cs | 12 -- .../Win32ControlsModule/Backend/Controls.cs | 3 +- .../Win32ControlsModule/Frontend/IButton.cs | 5 +- .../Win32ControlsModule/Frontend/ICheckBox.cs | 4 +- .../Win32ControlsModule/Frontend/IComboBox.cs | 4 +- .../Frontend/IComboBoxEx.cs | 4 +- .../Win32ControlsModule/Frontend/IControl.cs | 6 - .../Frontend/IDateTimePicker.cs | 4 +- .../Win32ControlsModule/Frontend/IEdit.cs | 4 +- .../Win32ControlsModule/Frontend/IHotKey.cs | 4 +- .../Frontend/IIpAddress.cs | 4 +- .../Win32ControlsModule/Frontend/ILink.cs | 4 +- .../Win32ControlsModule/Frontend/IListBox.cs | 4 +- .../Win32ControlsModule/Frontend/IListView.cs | 4 +- .../Frontend/IMonthCalendar.cs | 4 +- .../Frontend/IRadioButton.cs | 4 +- .../Frontend/ITabControl.cs | 4 +- .../Win32ControlsModule/Frontend/IToolbar.cs | 4 +- .../Win32ControlsModule/Frontend/ITrackBar.cs | 4 +- .../Win32ControlsModule/Frontend/ITreeView.cs | 4 +- .../Win32ControlsModule/Frontend/IUpDown.cs | 4 +- src/ProjectTemplates/EasyWinApp/Behavior.cs | 2 +- .../SimpleEasyWinApp/Program.cs | 2 +- src/Sample/Program.cs | 4 +- src/creators_vision.md | 2 +- src/system_design.md | 6 +- 52 files changed, 390 insertions(+), 252 deletions(-) create mode 100644 src/EasyWindowsApplication/Core/Surfaces/IInputFeed.cs create mode 100644 src/EasyWindowsApplication/Core/Surfaces/SurfaceInputHub.cs create mode 100644 src/EasyWindowsApplication/Core/Surfaces/TapChainCounter.cs create mode 100644 src/EasyWindowsApplication/Share/IInputSurface.cs create mode 100644 src/EasyWindowsApplication/Share/Input/SurfaceInputExtensions.cs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6b5a4e..778b2ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ El framework está organizado en módulos bajo `src/EasyWindowsApplication/`: | `Core/Windowing` | `internal` | `WindowImpl`, `AlternativeWindowImpl`, `Procedures` (`CreateMainWindow` con `WS_OVERLAPPEDWINDOW` + `CreateAlternativeWindow` con `WS_POPUP|CAPTION|SYSMENU`, registro `HandleRegistry.RegisterRouter` + `WndProcTrampoline`), `UserControl`, `Win32.cs` (P/Invoke parcial `[LibraryImport]`), `Entities.cs` (`MSG/POINT/NMHDR/…`), `Enums.cs`, `Constants.cs` (stub: "Window-specific constants are in Core") | | `Core/LayoutEngine` | `internal` | `LayoutEngine` (batching `DeferWindowPos` anti-flicker), `ILayoutStrategy`/`ILayoutable`, `LayoutBuilderImpl`, `ContentBuilderImpl`, `WindowConfigBuilder`, `ChildrenBuilderImpl` (usa `ControlActivatorRegistry`), `ViewBuilderImpl`, estrategias `GridLayoutStrategy`/`DockLayoutStrategy`/`HorizontalStackLayoutStrategy`/`VerticalStackLayoutStrategy` | | `Common` | `internal` | `ControlActivatorRegistry` (`Register/Create`, `RegisterFactory/TryGetFactory/CreateFactory/TryGetFactoryForControl`, `Shared` + `EnsureInitialized()` + `partial RegisterGeneratedActivators()`), `IControlActivator` (`RegisterActivators`), `INativeHandleFactory` (`CreateHandle(parentHwnd, control, registry)`), `Win32Helpers` (`HIWORD/LOWORD` usado por `MasterRouter`). Rompe ciclo `Common ↔ Core` sin `ModuleInitializer` (`CA2255`); usa `IIncrementalGenerator` + `InternalsVisibleTo` | -| `Share` | `public` | API usuario: jerarquía ventanas `IBaseWindow` → `IWindow`/`IAlternativeWindow`/`IView`, `IWindowConfig`/`IContentBuilder`/`ILayout`/`ILayoutBuilderAfterWindow.AlternativeWindow()`/`IViewBuilder` (controles custom), `ViewBase` (`Share/View.cs`, base para controles custom ej. `IpAddress`) + `View sealed class where T : class, IControl` (`Name/Margin/Padding/Width/Height/Background/Dock/OnClick/Text` + `PendingName` y lista `Action`), `IChildrenBuilder` (3 overloads: `View(Action>)` + `View(Func,View>)` + `View(Action)`), primitivas `LayoutLength`/`GridDefinitions`/`Thickness`/`Color`/`LayoutOptions`/`DockPosition`/`WindowPositionOnScreen`/`ScrollBarVisibility`, eventos `CancelEventArgs/WindowMoved/Resized/Resizing`, `IResourcesDictionary`/`IApplicationLayoutPhase` (Type-State) | +| `Share` | `public` | API usuario: jerarquía ventanas `IBaseWindow` → `IWindow`/`IAlternativeWindow`/`IView`, `IWindowConfig`/`IContentBuilder`/`ILayout`/`ILayoutBuilderAfterWindow.AlternativeWindow()`/`IViewBuilder` (controles custom), `ViewBase` (`Share/View.cs`, base para controles custom ej. `IpAddress`) + `View sealed class where T : class, IControl` (`Name/Margin/Padding/Width/Height/Background/Dock/Text` + `PendingName` y lista `Action`), `IChildrenBuilder` (3 overloads: `View(Action>)` + `View(Func,View>)` + `View(Action)`), primitivas `LayoutLength`/`GridDefinitions`/`Thickness`/`Color`/`LayoutOptions`/`DockPosition`/`WindowPositionOnScreen`/`ScrollBarVisibility`, eventos `CancelEventArgs/WindowMoved/Resized/Resizing`, `IResourcesDictionary`/`IApplicationLayoutPhase` (Type-State) | | `Share/Infrastructure` | `public` técnico | `[EditorBrowsable(Never)]`: `ControlAccess` (punto usado por `EasyBehaviorGenerator` vía `ControlAccess.Get(name)` / `GetWindow`) — único tipo en este namespace | | `Win32ControlsModule` | Frontend `public` / Backend `internal` | **Frontend:** 30 interfaces `IControl` → `IButton/ILabel/ICheckBox/IComboBox/…/ITreeView/IUpDown` + `ControlBuilderExtensions` + `StyleInterfaces` + clase `IpAddress : ViewBase` · **Backend:** `Button`/`Label` → `Core.ControlBase` (con `MeasureContent` + DPI scaling acoplado a `UiDefaultsProvider`), `Win32UiDefaults` (`IDefaultUiValues` concreto registrado en `Application.Initialize()`), `Win32NativeHandleFactory` (`INativeHandleFactory`), `Win32ControlActivator` (`IControlActivator`, `internal`, auto-registrado vía `ControlActivatorGenerator` + `EnsureInitialized()`), `Procedures.ControlProcedures` (capa GDI/User32: `GetDefaultFont/CreateFontFromSpec/GetDpiForWindowSafe/SendMessage/InvalidateDefaultFont/CreateControl`), `Win32.cs`/`Constants.cs`/`Entities.cs` del módulo | | `Generators` | `Analyzer` | `EasyBehaviorGenerator` (detecta `.Name("…")` en `View/Window/AlternativeWindow`, genera `EasyBehaviorExtensions` + `EAWIN001` duplicados + `EAWIN002` gate `UseWinApi()`, FQN `Share` + `GetTypeByMetadataName`) + `ControlActivatorGenerator` (descubre `IControlActivator` vía `AllInterfaces`, genera `ControlActivatorRegistrations.g.cs` con `new Win32ControlActivator().RegisterActivators(Shared)`, `InternalsVisibleTo`) | @@ -182,10 +182,10 @@ Documenta aquí las decisiones de arquitectura relevantes. Usa ADRs (Architectur - **Fluent API con interfaces de fase (Type-State)** — El IntelliSense guía el orden `Resources → Layout → Behavior → Initialize` mediante interfaces (`IApplicationLayoutPhase`, `IApplicationPostLayoutPhase`, `IApplicationPostBehaviorPhase`) ahora en `Share/` (antes `CoreModule/Frontend`). `WindowsApplication` devuelve `IApplicationLayoutPhase` público. `IApplicationPostLayoutPhase` expone tanto `Behavior()` como `Initialize()` para flujos mínimos (`Resources→Layout→Initialize` o `Layout→Initialize`). `Application.cs` tiene overloads sin parámetro `Layout()`/`Behavior()` que retornan `this` para encadenamiento condicional. `WindowsApplication` tiene `static constructor` que llama `ControlActivatorRegistry.EnsureInitialized()` (inicialización perezosa; segundo llamado en `Application.Initialize()` es no-op por flag `_inited`). - **Controles como interfaces** (`IButton`, `ILabel`, ...) — permite que el Source Generator genere tipos débilesmente acoplados y facilita testing/mocking. `Button`/`Label` heredan `Core.ControlBase` (antes `Share.Infrastructure.ControlBase`) y contienen lógica DPI (`MeasureContent` escala `PreferredHeight` 96→dpi actual vía `UiDefaultsProvider`). -- **MasterRouter** — 376 líneas: centraliza el bucle de mensajes con trampolín `[UnmanagedCallersOnly(CallConvs=[Stdcall])]` + lookup HWND-based (`HandleRegistry.ConcurrentDictionary`), `try/catch` que evita tumbar proceso, ciclo completo `WM.CLOSE/ENTERSIZEMOVE/EXITSIZEMOVE/SIZE/MOVE/ACTIVATE/HSCROLL/VSCROLL/MOUSEWHEEL/SETTINGCHANGE/DPICHANGED/ERASEBKGND/CTLCOLOR/DESTROY` (typed dispatch `WM.COMMAND→Click` + raw handlers + cleanup `Unregister/PostQuitMessage` + `WM.ERASEBKGND` con `LayoutGroup` backgrounds + `WM.CTLCOLOR*` con control brushes), despacha eventos tipados (`WM.COMMAND` → `Click`). El delegado estático sobrescrito fue eliminado (evita GC). Paint/Theme/DPI/scroll/lifecycle separados del GDI. +- **MasterRouter** — 376 líneas: centraliza el bucle de mensajes con trampolín `[UnmanagedCallersOnly(CallConvs=[Stdcall])]` + lookup HWND-based (`HandleRegistry.ConcurrentDictionary`), `try/catch` que evita tumbar proceso, ciclo completo `WM.CLOSE/ENTERSIZEMOVE/EXITSIZEMOVE/SIZE/MOVE/ACTIVATE/HSCROLL/VSCROLL/MOUSEWHEEL/SETTINGCHANGE/DPICHANGED/ERASEBKGND/CTLCOLOR/DESTROY` (typed dispatch `WM.COMMAND→MainTap` + raw handlers + cleanup `Unregister/PostQuitMessage` + `WM.ERASEBKGND` con `LayoutGroup` backgrounds + `WM.CTLCOLOR*` con control brushes), despacha entradas tipadas (`WM.COMMAND` → `IInputFeed.FeedTrigger(MainTap)`). El delegado estático sobrescrito fue eliminado (evita GC). Paint/Theme/DPI/scroll/lifecycle separados del GDI. - **HandleRegistry** — No es solo `ConcurrentDictionary`. Contiene `ConcurrentDictionary` (routers) + 5 diccionarios: `hwnd→WeakReference`, `name→WeakReference`, `name→IBaseWindow`, `hwnd→IBaseWindow`, `parent→List children`. Métodos: `TrackChildWindow/UnregisterWindowControls/RegisterWindow/UnregisterWindow/UnregisterWindowByHwnd/GetWindowByHwnd/GetByName/GetWindow/AllControlHandles`. - **ControlAccess / ControlActivatorRegistry / ControlActivatorGenerator** — `ControlAccess` sigue en `Share/Infrastructure` (usado por `EasyBehaviorGenerator`). `ControlActivatorRegistry` en `Common/` (genérico `Register/Create` + `RegisterFactory/TryGetFactory/CreateFactory/CreateFactoryFor/TryGetFactoryForControl` + `INativeHandleFactory` + `Shared` + `EnsureInitialized()` + `partial RegisterGeneratedActivators()`) rompe acoplamiento `Core → Win32ControlsModule` sin `ModuleInitializer` (`CA2255` en `Library`). `ControlActivatorGenerator` (`IIncrementalGenerator` en `Generators`, descubre `IControlActivator` en `Compilation` vía `GetTypeByMetadataName` + `AllInterfaces`, genera `ControlActivatorRegistrations.g.cs` con `new Win32ControlActivator().RegisterActivators(Shared)` y futuros plugins; usa `` para mantener `internal`. -- **View sealed class** — `Share/View` `public sealed class where T : class, IControl` (no `struct`; semántica de referencia en heap, GC, identidad por referencia) con lista `Action` + fluents (`Name/Margin/Padding/Width/Height/Background/Dock/OnClick/Text`). Triple overload en `IChildrenBuilder`: `View(Action>)` + `View(Func,View>)` + `View(Action)` para controles custom sin tipo genérico, instanciando `View(control)` vía `ControlActivatorRegistry`. +- **View sealed class** — `Share/View` `public sealed class where T : class, IControl` (no `struct`; semántica de referencia en heap, GC, identidad por referencia) con lista `Action` + fluents (`Name/Margin/Padding/Width/Height/Background/Dock/Text`). Triple overload en `IChildrenBuilder`: `View(Action>)` + `View(Func,View>)` + `View(Action)` para controles custom sin tipo genérico, instanciando `View(control)` vía `ControlActivatorRegistry`. - **`UseWinApi()` como bifurcación explícita** — gate **solo compile-time** (`EAWIN002`): `SettingsBuilderImpl.UseWinApi()` retorna `this` (no-op en runtime). El Source Generator decide si emite accessors tipados. El mecanismo runtime real es `Core/UiDefaults` (`UiDefaultsProvider.Set(new Win32UiDefaults())` en `Application.Initialize()` antes de `InitCommonControlsEx` y registro de ventanas). `UseWinApi()` y `UiDefaults` son conceptos separados. `IconGenerator` migró `Share/Infrastructure` → `Core/` (ahora `Core.IconGenerator`, IcoGen usa `EasyWindowsApplication.Core`). - **UiDefaults y DPI** — `Core/UiDefaults/` (7 archivos) + `Backend/Win32UiDefaults` + `Backend/Controls.cs` (`Button.MeasureContent` escala `PreferredHeight` vía `UiDefaultsProvider.Current.GetFor()` y `GetDpiForWindow` 96→actual; `ControlProcedures.GetDefaultFont` lee `FontSpec` vía `UiDefaultsProvider.Current.DefaultFont` → `CreateFontFromSpec` → `GetDpiForSystemSafe`). Ver diagrama en [Arquitectura de módulos](#arquitectura-de-modulos). - **Separación Procedures** — `Core/Procedures.cs` (`RunMessageLoop`: `while(GetMessageW>0){Translate/Dispatch}`) pertenece al núcleo; `Core/Windowing/Procedures.cs` (`CreateMainWindow/CreateAlternativeWindow` con `RegisterClassExW` + `WndProcTrampoline`) pertenece a windowing; `Win32ControlsModule/Backend/Procedures.cs` (`ControlProcedures`: `GetDefaultFont/CreateFontFromSpec/GetDpiForWindowSafe/SendMessage/CreateControl`) es la capa GDI/User32. Mezclar message loop con GDI es el error Win32 más común — esta separación debe preservarse. diff --git a/README.md b/README.md index b076bd0..f303b85 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ WindowsApplication .Behavior(bh => { var btn = bh.BtnGuardar; // tipado por Source Generator (View wrapper) - btn.OnClick(() => + btn.OnMainTap(() => { counter++; btn.Text = $"Contador: {counter}"; diff --git a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs index a371643..3fcf2bf 100644 --- a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs +++ b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs @@ -214,7 +214,7 @@ private static bool IsNameCandidate(SyntaxNode node) { var typeArg = generic.TypeArgumentList.Arguments[0]; var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg).Type; - if (typeSymbol == null) return null; + if (typeSymbol is null) return null; return new NamedInfo( name, @@ -235,7 +235,7 @@ private static bool IsNameCandidate(SyntaxNode node) { var typeArg = altGeneric.TypeArgumentList.Arguments[0]; var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg).Type; - if (typeSymbol == null) return null; + if (typeSymbol is null) return null; var compilation = context.SemanticModel.Compilation; var iBaseWindow = compilation.GetTypeByMetadataName( diff --git a/src/EasyWindowsApplication/Core/ControlBase.cs b/src/EasyWindowsApplication/Core/ControlBase.cs index 393144e..289f50b 100644 --- a/src/EasyWindowsApplication/Core/ControlBase.cs +++ b/src/EasyWindowsApplication/Core/ControlBase.cs @@ -2,7 +2,9 @@ using System.Runtime.InteropServices; using EasyWindowsApplication.Core.LayoutEngine; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; using EasyWindowsApplication.Win32ControlsModule.Backend; using EasyWindowsApplication.Win32ControlsModule.Frontend; using static EasyWindowsApplication.Core.Win32; @@ -10,10 +12,13 @@ namespace EasyWindowsApplication.Core; [EditorBrowsable(EditorBrowsableState.Never)] -public abstract class ControlBase : IControl, IClickEventSource, ILayoutable, IDockable +public abstract class ControlBase : IControl, IInputSurface, IInputFeed, ILayoutable, IDockable { internal ControlBase() { } + private readonly SurfaceInputHub _hub = new(); + void IInputFeed.FeedTrigger(Type trigger, int count) => _hub.Fire(trigger, count); + public nint Hwnd { get; internal set; } public string Name { get; set; } = ""; @@ -26,22 +31,6 @@ public event EventHandler? Deactivated { add { } remove { } } private float _x, _y, _w, _h; - internal event Action? InternalClick; - - void IClickEventSource.RaiseClickInternal() => InternalClick?.Invoke(); - void IClickEventSource.AddClickHandler(Action handler) => InternalClick += handler; - - public event Action? Clicked - { - add => InternalClick += value; - remove => InternalClick -= value; - } - - public void OnClick(Action handler) - { - InternalClick += handler; - } - public nint OnMessage(uint msg, Win32MessageHandler handler) { Router.RegisterHandler(Hwnd, msg, handler); @@ -309,4 +298,15 @@ private static int MeasureSurrogatePairFallback(nint hdc, string text, int index } return 8; } + + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger + => _hub.AddSpatial(typeof(TTrigger), handler); + + public void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount + => _hub.AddCounted(typeof(TTrigger), typeof(TCount), handler); + + public void OnInputWithoutSpatialPosition(Action handler) where TTrigger : WithoutSpatialPositionTrigger + => _hub.AddNonSpatial(typeof(TTrigger), handler); } diff --git a/src/EasyWindowsApplication/Core/MasterRouter.cs b/src/EasyWindowsApplication/Core/MasterRouter.cs index 3c1683a..7d80368 100644 --- a/src/EasyWindowsApplication/Core/MasterRouter.cs +++ b/src/EasyWindowsApplication/Core/MasterRouter.cs @@ -2,7 +2,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using EasyWindowsApplication.Common; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Core.Windowing; +using EasyWindowsApplication.Share.Input; using EasyWindowsApplication.Share; using EasyWindowsApplication.Win32ControlsModule.Frontend; @@ -337,8 +339,11 @@ private void DispatchTypedEvent(uint msg, nint controlHwnd, nint wParam, nint lP { case WM.COMMAND: nint code = Win32Helpers.HIWORD(wParam); - if ((code == BN.CLICKED || code == BN.DOUBLECLICKED) && control is IClickEventSource clickable) - clickable.RaiseClickInternal(); + if (control is IInputFeed feed) + { + if (code == BN.CLICKED) feed.FeedTrigger(typeof(MainTap), 1); + if (code == BN.DOUBLECLICKED) feed.FeedTrigger(typeof(MainTap), 2); + } break; } } diff --git a/src/EasyWindowsApplication/Core/Menus/MenuItem.cs b/src/EasyWindowsApplication/Core/Menus/MenuItem.cs index a0c517f..e60f5c3 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuItem.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuItem.cs @@ -1,9 +1,14 @@ +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; namespace EasyWindowsApplication.Core.Menus; -internal sealed class MenuItem : IMenuItem +internal sealed class MenuItem : IMenuItem, IInputFeed { + private readonly SurfaceInputHub _hub = new(); + void IInputFeed.FeedTrigger(Type trigger, int count) => _hub.Fire(trigger, count); + public string Name { get; set; } = ""; public string Text { get; set; } = ""; public bool IsEnabled { get; set; } = true; @@ -20,9 +25,14 @@ internal sealed class MenuItem : IMenuItem public int GridColumnSpan { get; set; } public Color? BackgroundColor { get; set; } - public event Action? Clicked; + public void OnInputWithoutSpatialPosition(Action handler) where TTrigger : WithoutSpatialPositionTrigger + => _hub.AddNonSpatial(typeof(TTrigger), handler); - public void OnClick(Action handler) => Clicked += handler; + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger + => _hub.AddSpatial(typeof(TTrigger), handler); - internal void RaiseClicked() => Clicked?.Invoke(); + public void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount + => _hub.AddCounted(typeof(TTrigger), typeof(TCount), handler); } diff --git a/src/EasyWindowsApplication/Core/Menus/MenuModel.cs b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs index 62e1805..59eec7d 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuModel.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuModel.cs @@ -8,7 +8,7 @@ internal sealed class MenuItemModel internal bool IsChecked; internal bool IsSeparator; internal List Children = []; - internal Action? OnClick; + internal Action? OnSelected; } internal sealed class MenuModel diff --git a/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs b/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs index bdce76b..6182253 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuSeparatorItem.cs @@ -16,10 +16,4 @@ internal sealed class MenuSeparatorItem : IMenuItemSeparator public int GridRowSpan { get; set; } public int GridColumnSpan { get; set; } public Color? BackgroundColor { get; set; } - - public event Action? Clicked; - - public void OnClick(Action handler) => Clicked += handler; - - internal void RaiseClicked() => Clicked?.Invoke(); } diff --git a/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs index fac5986..722f8c5 100644 --- a/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs +++ b/src/EasyWindowsApplication/Core/Menus/MenuSurface.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; namespace EasyWindowsApplication.Core.Menus; @@ -51,10 +53,6 @@ internal void SetIconAnchor(nint trayHwnd, uint trayId) public int GridColumnSpan { get; set; } public Color? BackgroundColor { get; set; } - public event Action? Clicked; - - public void OnClick(Action handler) => Clicked += handler; - public void Show() { if (_iconAnchor is not null) @@ -142,10 +140,7 @@ private MenuItemModel ToItemModel(IViewSurface surface, ContentModel? sub) Text = item.Text, IsEnabled = item.IsEnabled, IsChecked = item.IsChecked, - OnClick = () => - { - if (item is MenuItem concrete) concrete.RaiseClicked(); - } + OnSelected = () => ((IInputFeed)item).FeedTrigger(typeof(MainTap), 1) }; if (sub is not null) { @@ -162,7 +157,6 @@ private MenuItemModel ToItemModel(IViewSurface surface, ContentModel? sub) private void ShowAt(int x, int y, uint align) { var model = EnsureMaterialized(); - int id = Win32MenuEngine.Show(_ownerHwnd, model, x, y, align); - if (id > 0) Clicked?.Invoke(); + Win32MenuEngine.Show(_ownerHwnd, model, x, y, align); } } diff --git a/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs index 4e59167..914ad6a 100644 --- a/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs +++ b/src/EasyWindowsApplication/Core/Menus/Win32MenuEngine.cs @@ -31,7 +31,7 @@ internal static bool Dispatch(MenuModel model, int id) if (id <= 0) return false; var item = model.FindById(id); if (item is null || !item.IsEnabled || item.IsSeparator) return false; - item.OnClick?.Invoke(); + item.OnSelected?.Invoke(); return true; } diff --git a/src/EasyWindowsApplication/Core/Surfaces/IInputFeed.cs b/src/EasyWindowsApplication/Core/Surfaces/IInputFeed.cs new file mode 100644 index 0000000..094b150 --- /dev/null +++ b/src/EasyWindowsApplication/Core/Surfaces/IInputFeed.cs @@ -0,0 +1,10 @@ +namespace EasyWindowsApplication.Core.Surfaces; + +/// +/// Punto único de entrada OS → hub. Lo implementan las superficies con +/// alimentación externa (, items de menú, ventanas). +/// +internal interface IInputFeed +{ + void FeedTrigger(Type trigger, int count); +} diff --git a/src/EasyWindowsApplication/Core/Surfaces/SurfaceInputHub.cs b/src/EasyWindowsApplication/Core/Surfaces/SurfaceInputHub.cs new file mode 100644 index 0000000..e857d01 --- /dev/null +++ b/src/EasyWindowsApplication/Core/Surfaces/SurfaceInputHub.cs @@ -0,0 +1,65 @@ +namespace EasyWindowsApplication.Core.Surfaces; + +/// +/// Motor de dispatch unificado extraído de SystemTrayImpl. +/// Todo corre en el hilo de UI (message loop); sin locks. +/// +internal sealed class SurfaceInputHub +{ + private static readonly Type[] TapCounts = + [ + typeof(Share.Input.OneTap), typeof(Share.Input.TwoTap), typeof(Share.Input.ThreeTap), + typeof(Share.Input.FourTap), typeof(Share.Input.FiveTap), typeof(Share.Input.SixTap), + typeof(Share.Input.SevenTap), typeof(Share.Input.EightTap), typeof(Share.Input.NineTap), + typeof(Share.Input.TenTap) + ]; + + private readonly Dictionary> _spatial = []; + private readonly Dictionary<(Type Trigger, Type Count), List> _counted = []; + private readonly Dictionary> _nonSpatial = []; + + public void AddSpatial(Type trigger, Action handler) + { + if (!_spatial.TryGetValue(trigger, out var list)) + { + list = []; + _spatial[trigger] = list; + } + list.Add(handler); + } + + public void AddCounted(Type trigger, Type count, Action handler) + { + var key = (trigger, count); + if (!_counted.TryGetValue(key, out var list)) + { + list = []; + _counted[key] = list; + } + list.Add(handler); + } + + public void AddNonSpatial(Type trigger, Action handler) + { + if (!_nonSpatial.TryGetValue(trigger, out var list)) + { + list = []; + _nonSpatial[trigger] = list; + } + list.Add(handler); + } + + public void Fire(Type trigger, int count) + { + if (_spatial.TryGetValue(trigger, out var list)) + foreach (var handler in list.ToArray()) handler(); + if (count >= 1 && count <= TapCounts.Length) + { + var key = (trigger, TapCounts[count - 1]); + if (_counted.TryGetValue(key, out var counted)) + foreach (var handler in counted.ToArray()) handler(); + } + if (_nonSpatial.TryGetValue(trigger, out var nonSpatial)) + foreach (var handler in nonSpatial.ToArray()) handler(); + } +} diff --git a/src/EasyWindowsApplication/Core/Surfaces/TapChainCounter.cs b/src/EasyWindowsApplication/Core/Surfaces/TapChainCounter.cs new file mode 100644 index 0000000..52d1ac0 --- /dev/null +++ b/src/EasyWindowsApplication/Core/Surfaces/TapChainCounter.cs @@ -0,0 +1,42 @@ +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Core.Surfaces; + +/// +/// Contador de taps encadenados extraído de SystemTrayBroker.Fire/IsChainable. +/// Una sola fuente de verdad de timing para el broker del tray y futuros adaptadores. +/// +internal sealed class TapChainCounter +{ + private Type? _lastTrigger; + private int _lastCount; + private int _lastTick; + + public int Next(Type trigger, int nowTick, uint doubleClickTimeMs) + { + int count = 1; + if (IsChainable(trigger) && trigger == _lastTrigger) + { + if (unchecked(nowTick - _lastTick) <= (int)doubleClickTimeMs) + count = Math.Min(_lastCount + 1, 10); + } + if (IsChainable(trigger)) + { + _lastTrigger = trigger; + _lastCount = count; + _lastTick = nowTick; + } + else + { + _lastTrigger = null; + _lastCount = 0; + } + return count; + } + + public static bool IsChainable(Type trigger) + => trigger == typeof(MainTap) + || trigger == typeof(AlternativeTap1) + || trigger == typeof(AlternativeTap2) + || trigger == typeof(LongTap); +} diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs index d0aace8..cfdc535 100644 --- a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayBroker.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share.Input; namespace EasyWindowsApplication.Core.SystemTray; @@ -13,7 +14,6 @@ internal sealed class SystemTrayBroker private const uint WM_MOUSEMOVE = 0x0200; private const uint WM_LBUTTONDOWN = 0x0201; private const uint WM_LBUTTONUP = 0x0202; - private const uint WM_LBUTTONDBLCLK = 0x0203; private const uint WM_RBUTTONUP = 0x0205; private const uint WM_MBUTTONUP = 0x0208; @@ -25,11 +25,9 @@ internal sealed class SystemTrayBroker private nint _icon; private bool _disposed; - private Type? _lastTrigger; - private int _lastCount; - private int _lastTick; private int _lastHoverTick; private bool _holding; + private readonly TapChainCounter _counter = new(); internal nint Hwnd { get; } internal uint CallbackMessage => _callbackMessage; @@ -132,7 +130,8 @@ private nint OnCallback(nint wParam, nint lParam) case WM_MOUSEMOVE: OnHover(); break; case WM_LBUTTONDOWN: OnButtonDown(); break; case WM_LBUTTONUP: OnButtonUp(); break; - case WM_LBUTTONDBLCLK: Fire(typeof(MainDoubleTap)); break; + // WM_LBUTTONDBLCLK se ignora a propósito: el doble-tap ya se codifica + // como cadena MainTap + TwoTap en el segundo WM_LBUTTONUP (§4.2). case WM_RBUTTONUP: if (IsCursorOverIcon()) Fire(typeof(AlternativeTap1)); break; case WM_MBUTTONUP: if (IsCursorOverIcon()) Fire(typeof(AlternativeTap2)); break; case WM.CONTEXTMENU: OnContextMenuKey(); break; @@ -228,33 +227,10 @@ private void OnHover() private void Fire(Type trigger) { - int count = 1; - if (IsChainable(trigger) && trigger == _lastTrigger) - { - int now = Environment.TickCount; - if (unchecked(now - _lastTick) <= (int)Win32.GetDoubleClickTime()) - count = Math.Min(_lastCount + 1, 10); - } - if (IsChainable(trigger)) - { - _lastTrigger = trigger; - _lastCount = count; - _lastTick = Environment.TickCount; - } - else - { - _lastTrigger = null; - _lastCount = 0; - } + int count = _counter.Next(trigger, Environment.TickCount, Win32.GetDoubleClickTime()); _dispatch(trigger, count); } - private static bool IsChainable(Type trigger) - => trigger == typeof(MainTap) - || trigger == typeof(AlternativeTap1) - || trigger == typeof(AlternativeTap2) - || trigger == typeof(LongTap); - private NOTIFYICONDATAW NewData(uint flags) { return new NOTIFYICONDATAW diff --git a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs index d2280ee..849bb11 100644 --- a/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs +++ b/src/EasyWindowsApplication/Core/SystemTray/SystemTrayImpl.cs @@ -1,3 +1,4 @@ +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; using EasyWindowsApplication.Share.Input; @@ -5,16 +6,8 @@ namespace EasyWindowsApplication.Core.SystemTray; internal sealed class SystemTrayImpl : ISystemTray { - private static readonly Type[] TapCounts = - { - typeof(OneTap), typeof(TwoTap), typeof(ThreeTap), typeof(FourTap), typeof(FiveTap), - typeof(SixTap), typeof(SevenTap), typeof(EightTap), typeof(NineTap), typeof(TenTap) - }; - private readonly SystemTrayBroker _broker; - private readonly Dictionary> _spatial = new(); - private readonly Dictionary<(Type Trigger, Type Count), List> _counted = new(); - private readonly Dictionary> _nonSpatial = new(); + private readonly SurfaceInputHub _hub = new(); private string _tooltip = ""; internal SystemTrayBroker Broker => _broker; @@ -34,10 +27,6 @@ internal sealed class SystemTrayImpl : ISystemTray public int GridColumnSpan { get; set; } public Color? BackgroundColor { get; set; } - public event Action? Clicked; - - public void OnClick(Action handler) => Clicked += handler; - public void Visibility(bool visible) => _broker.SetVisible(visible); public ISystemTray Tooltip(string text) @@ -59,56 +48,24 @@ public ISystemTray DismissNotification() return this; } - public ISystemTray OnInputWithSpatialPosition(Action handler) + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger { - Add(_spatial, typeof(TTrigger), handler); - return this; + _hub.AddSpatial(typeof(TTrigger), handler); } - public ISystemTray OnInputWithSpatialPosition(Action handler) + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger where TCount : ITapCount { - var key = (typeof(TTrigger), typeof(TCount)); - if (!_counted.TryGetValue(key, out var list)) - { - list = new List(); - _counted[key] = list; - } - list.Add(handler); - return this; + _hub.AddCounted(typeof(TTrigger), typeof(TCount), handler); } - public ISystemTray OnInputWithoutSpatialPosition(Action handler) + public void OnInputWithoutSpatialPosition(Action handler) where TTrigger : WithoutSpatialPositionTrigger { - Add(_nonSpatial, typeof(TTrigger), handler); - return this; - } - - internal void DispatchTrigger(Type trigger, int count) - { - if (trigger == typeof(MainTap)) Clicked?.Invoke(); - if (_spatial.TryGetValue(trigger, out var list)) - foreach (var handler in list.ToArray()) handler(); - if (count >= 1 && count <= TapCounts.Length) - { - var key = (trigger, TapCounts[count - 1]); - if (_counted.TryGetValue(key, out var counted)) - foreach (var handler in counted.ToArray()) handler(); - } - if (_nonSpatial.TryGetValue(trigger, out var nonSpatial)) - foreach (var handler in nonSpatial.ToArray()) handler(); + _hub.AddNonSpatial(typeof(TTrigger), handler); } - private static void Add(Dictionary> map, Type trigger, Action handler) - { - if (!map.TryGetValue(trigger, out var list)) - { - list = new List(); - map[trigger] = list; - } - list.Add(handler); - } + internal void DispatchTrigger(Type trigger, int count) => _hub.Fire(trigger, count); } diff --git a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs index 8bdd1fb..741ed02 100644 --- a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs @@ -1,13 +1,29 @@ using EasyWindowsApplication.Common; using EasyWindowsApplication.Core; using EasyWindowsApplication.Core.LayoutEngine; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; using EasyWindowsApplication.Win32ControlsModule.Frontend; namespace EasyWindowsApplication.Core.Windowing; -internal sealed class AlternativeWindowImpl : IAlternativeWindow +internal sealed class AlternativeWindowImpl : IAlternativeWindow, IInputFeed { + private readonly SurfaceInputHub _hub = new(); + void IInputFeed.FeedTrigger(Type trigger, int count) => _hub.Fire(trigger, count); + + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger + => _hub.AddSpatial(typeof(TTrigger), handler); + + public void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount + => _hub.AddCounted(typeof(TTrigger), typeof(TCount), handler); + + public void OnInputWithoutSpatialPosition(Action handler) where TTrigger : WithoutSpatialPositionTrigger + => _hub.AddNonSpatial(typeof(TTrigger), handler); + public nint Hwnd { get; } public string Name { get; } public nint OwnerHwnd { get; } @@ -18,7 +34,7 @@ public string Title set { _title = value ?? ""; - if (Hwnd != 0) EasyWindowsApplication.Core.Win32.SetWindowText(Hwnd, _title); + if (Hwnd != 0) Core.Win32.SetWindowText(Hwnd, _title); } } private int _width; @@ -30,7 +46,7 @@ public int Width if (_width == value) return; _width = value; if (Hwnd != 0) - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); } } private int _height; @@ -42,7 +58,7 @@ public int Height if (_height == value) return; _height = value; if (Hwnd != 0) - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); } } private WindowPositionOnScreen _positionMode; @@ -56,21 +72,21 @@ public WindowPositionOnScreen PositionMode if (Hwnd != 0 && _positionMode == WindowPositionOnScreen.Center) { // Reuse Center logic similar to WindowImpl - nint monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); - if (monitor == 0) monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); + nint monitor = Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); + if (monitor == 0) monitor = Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); if (monitor != 0) { MONITORINFO mi = new() { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf() }; - if (EasyWindowsApplication.Core.Win32.GetMonitorInfoW(monitor, ref mi)) + if (Core.Win32.GetMonitorInfoW(monitor, ref mi)) { - EasyWindowsApplication.Core.Win32.GetWindowRect(Hwnd, out RECT wr); + Core.Win32.GetWindowRect(Hwnd, out RECT wr); int winW = wr.Right - wr.Left; int winH = wr.Bottom - wr.Top; int workW = mi.rcWork.Right - mi.rcWork.Left; int workH = mi.rcWork.Bottom - mi.rcWork.Top; int x = mi.rcWork.Left + (workW - winW) / 2; int y = mi.rcWork.Top + (workH - winH) / 2; - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); + Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); } } } @@ -100,15 +116,15 @@ internal AlternativeWindowImpl(nint hwnd, nint ownerHwnd, string name, string ti public void Show() { - EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.SHOW); + Win32.ShowWindow(Hwnd, SW.SHOW); if (!_hasLoaded) { _hasLoaded = true; Loaded?.Invoke(this, EventArgs.Empty); } } - public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); - public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + public void Hide() => Win32.ShowWindow(Hwnd, SW.HIDE); + public void Close() => Win32.DestroyWindow(Hwnd); public void Visibility(bool visible) { if (visible) Show(); @@ -147,7 +163,7 @@ internal void MaterializeContent(WindowModel window, HandleRegistry registry, Ma if (availW <= 0) availW = window.Width; if (availH <= 0) availH = window.Height; - var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + var engine = new LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); engine.Execute(layoutables, availW, availH, content.Spacing, content.Padding); _materializedChildren = layoutables; diff --git a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs index 1b75727..548598b 100644 --- a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs @@ -1,13 +1,29 @@ using EasyWindowsApplication.Common; using EasyWindowsApplication.Core; using EasyWindowsApplication.Core.LayoutEngine; +using EasyWindowsApplication.Core.Surfaces; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Input; using EasyWindowsApplication.Win32ControlsModule.Frontend; namespace EasyWindowsApplication.Core.Windowing; -internal sealed class WindowImpl : IWindow +internal sealed class WindowImpl : IWindow, IInputFeed { + private readonly SurfaceInputHub _hub = new(); + void IInputFeed.FeedTrigger(Type trigger, int count) => _hub.Fire(trigger, count); + + public void OnInputWithSpatialPosition(Action handler) where TTrigger : ISpatialPositionTrigger + => _hub.AddSpatial(typeof(TTrigger), handler); + + public void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount + => _hub.AddCounted(typeof(TTrigger), typeof(TCount), handler); + + public void OnInputWithoutSpatialPosition(Action handler) where TTrigger : WithoutSpatialPositionTrigger + => _hub.AddNonSpatial(typeof(TTrigger), handler); + public nint Hwnd { get; } public string Name { get; } private string _title; @@ -17,7 +33,7 @@ public string Title set { _title = value ?? ""; - if (Hwnd != 0) EasyWindowsApplication.Core.Win32.SetWindowText(Hwnd, _title); + if (Hwnd != 0) Core.Win32.SetWindowText(Hwnd, _title); } } @@ -30,7 +46,7 @@ public float Width if (_width == value) return; _width = value; if (Hwnd != 0) - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); } } @@ -43,7 +59,7 @@ public float Height if (_height == value) return; _height = value; if (Hwnd != 0) - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); } } @@ -93,9 +109,9 @@ internal WindowImpl(nint hwnd, string name, string title, float width, float hei _positionMode = position; } - public void Show() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.SHOW); - public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); - public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + public void Show() => Win32.ShowWindow(Hwnd, SW.SHOW); + public void Hide() => Win32.ShowWindow(Hwnd, SW.HIDE); + public void Close() => Win32.DestroyWindow(Hwnd); public void Visibility(bool visible) { if (visible) Show(); @@ -105,14 +121,14 @@ public void Visibility(bool visible) public void Center() { if (Hwnd == 0) return; - nint monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); - if (monitor == 0) monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); + nint monitor = Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); + if (monitor == 0) monitor = Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); if (monitor == 0) return; MONITORINFO mi = new() { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf() }; - if (!EasyWindowsApplication.Core.Win32.GetMonitorInfoW(monitor, ref mi)) return; + if (!Core.Win32.GetMonitorInfoW(monitor, ref mi)) return; - EasyWindowsApplication.Core.Win32.GetWindowRect(Hwnd, out RECT wr); + Core.Win32.GetWindowRect(Hwnd, out RECT wr); int winW = wr.Right - wr.Left; int winH = wr.Bottom - wr.Top; @@ -122,13 +138,13 @@ public void Center() int x = mi.rcWork.Left + (workW - winW) / 2; int y = mi.rcWork.Top + (workH - winH) / 2; - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); + Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); } - public void Maximize() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.MAXIMIZE); - public void Minimize() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.MINIMIZE); - public void Restore() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.RESTORE); - public void Focus() => EasyWindowsApplication.Core.Win32.SetForegroundWindow(Hwnd); + public void Maximize() => Core.Win32.ShowWindow(Hwnd, SW.MAXIMIZE); + public void Minimize() => Core.Win32.ShowWindow(Hwnd, SW.MINIMIZE); + public void Restore() => Core.Win32.ShowWindow(Hwnd, SW.RESTORE); + public void Focus() => Core.Win32.SetForegroundWindow(Hwnd); internal void RaiseLoaded() => Loaded?.Invoke(this, EventArgs.Empty); internal bool RaiseClosing() @@ -152,15 +168,15 @@ public void ScrollTo(int x, int y) // Actualizar SCROLLINFO if (_scrollConfig is not null) { - if (_maxScrollY > 0 || _scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) + if (_maxScrollY > 0 || _scrollConfig.VerticalScrollBarVisibility is ScrollBarVisibility.Always) { var siV = new SCROLLINFO { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), fMask = SIF.POS, nPos = _scrollY }; - EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 1, ref siV, true); + Core.Win32.SetScrollInfo(Hwnd, 1, ref siV, true); } - if (_maxScrollX > 0 || _scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) + if (_maxScrollX > 0 || _scrollConfig.HorizontalScrollBarVisibility is ScrollBarVisibility.Always) { var siH = new SCROLLINFO { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), fMask = SIF.POS, nPos = _scrollX }; - EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 0, ref siH, true); + Core.Win32.SetScrollInfo(Hwnd, 0, ref siH, true); } } @@ -174,8 +190,8 @@ public void ScrollTo(int x, int y) private void ApplyScrollOffset() { - if (_materializedChildren == null || _materializedChildren.Count == 0) return; - nint hdwp = EasyWindowsApplication.Core.Win32.BeginDeferWindowPos(_materializedChildren.Count); + if (_materializedChildren is null || _materializedChildren.Count == 0) return; + nint hdwp = Core.Win32.BeginDeferWindowPos(_materializedChildren.Count); bool batching = hdwp != 0; if (batching) { @@ -185,30 +201,31 @@ private void ApplyScrollOffset() { int x = (int)cb._arrangedX - _scrollX; int y = (int)cb._arrangedY - _scrollY; - nint next = EasyWindowsApplication.Core.Win32.DeferWindowPos(hdwp, cb.Hwnd, 0, x, y, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); + nint next = Core.Win32.DeferWindowPos(hdwp, cb.Hwnd, 0, x, y, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); if (next != 0) hdwp = next; else batching = false; } } if (batching) { - EasyWindowsApplication.Core.Win32.EndDeferWindowPos(hdwp); + Core.Win32.EndDeferWindowPos(hdwp); return; } - if (hdwp != 0) EasyWindowsApplication.Core.Win32.EndDeferWindowPos(hdwp); + if (hdwp != 0) Core.Win32.EndDeferWindowPos(hdwp); } foreach (var child in _materializedChildren) { if (child is ControlBase cb && cb.Hwnd != 0) { - EasyWindowsApplication.Core.Win32.SetWindowPos(cb.Hwnd, 0, (int)cb._arrangedX - _scrollX, (int)cb._arrangedY - _scrollY, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); + Core.Win32.SetWindowPos(cb.Hwnd, 0, (int)cb._arrangedX - _scrollX, (int)cb._arrangedY - _scrollY, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); } } } internal void HandleVScroll(int request, int thumbPos) { - if (_scrollConfig == null) return; + if (_scrollConfig is null) return; + int newPos = _scrollY; switch (request) { @@ -236,7 +253,7 @@ internal void HandleVScroll(int request, int thumbPos) internal void HandleHScroll(int request, int thumbPos) { - if (_scrollConfig == null) return; + if (_scrollConfig is null) return; int newPos = _scrollX; switch (request) { @@ -264,9 +281,9 @@ internal void HandleHScroll(int request, int thumbPos) internal void HandleMouseWheel(int delta) { - if (_scrollConfig == null) return; + if (_scrollConfig is null) return; // Rueda vertical por defecto; si orientación es Horizontal, scrollear horizontal - bool isHorizontal = _scrollConfig.Orientation == ScrollOrientation.Horizontal; + bool isHorizontal = _scrollConfig.Orientation is ScrollOrientation.Horizontal; if (isHorizontal) { int newX = _scrollX - delta * 30 / 120; @@ -281,7 +298,7 @@ internal void HandleMouseWheel(int delta) private void ConfigureScrollbars() { - if (_scrollConfig == null || Hwnd == 0) return; + if (_scrollConfig is null || Hwnd == 0) return; // Calcular tamaños totales del contenido int totalH = 0; @@ -306,25 +323,25 @@ private void ConfigureScrollbars() if (wantsV) { needV = totalH > _clientH; - if (_scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Never) needV = false; - else if (_scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) needV = true; + if (_scrollConfig.VerticalScrollBarVisibility is ScrollBarVisibility.Never) needV = false; + else if (_scrollConfig.VerticalScrollBarVisibility is ScrollBarVisibility.Always) needV = true; } if (wantsH) { needH = totalW > _clientW; - if (_scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Never) needH = false; - else if (_scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) needH = true; + if (_scrollConfig.HorizontalScrollBarVisibility is ScrollBarVisibility.Never) needH = false; + else if (_scrollConfig.HorizontalScrollBarVisibility is ScrollBarVisibility.Always) needH = true; } // Activar estilos WS_HSCROLL/VSCROLL - int style = EasyWindowsApplication.Core.Win32.GetWindowLongW(Hwnd, GWL.STYLE); + int style = Core.Win32.GetWindowLongW(Hwnd, GWL.STYLE); int newStyle = style; if (needV) newStyle |= (int)WS.VSCROLL; else newStyle &= ~(int)WS.VSCROLL; if (needH) newStyle |= (int)WS.HSCROLL; else newStyle &= ~(int)WS.HSCROLL; if (newStyle != style) { - EasyWindowsApplication.Core.Win32.SetWindowLongW(Hwnd, GWL.STYLE, newStyle); - EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE | SWP.NOSIZE | SWP.FRAMECHANGED); + Core.Win32.SetWindowLongW(Hwnd, GWL.STYLE, newStyle); + Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE | SWP.NOSIZE | SWP.FRAMECHANGED); } _maxScrollY = Math.Max(0, totalH - _clientH); @@ -335,7 +352,7 @@ private void ConfigureScrollbars() _scrollY = Math.Clamp(_scrollY, 0, _maxScrollY); // Configurar SCROLLINFO - if (needV || _scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) + if (needV || _scrollConfig.VerticalScrollBarVisibility is ScrollBarVisibility.Always) { var si = new SCROLLINFO { @@ -346,9 +363,9 @@ private void ConfigureScrollbars() nPage = (uint)Math.Max(0, _clientH), nPos = _scrollY }; - EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 1, ref si, true); + Core.Win32.SetScrollInfo(Hwnd, 1, ref si, true); } - if (needH || _scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) + if (needH || _scrollConfig.HorizontalScrollBarVisibility is ScrollBarVisibility.Always) { var si = new SCROLLINFO { @@ -359,7 +376,7 @@ private void ConfigureScrollbars() nPage = (uint)Math.Max(0, _clientW), nPos = _scrollX }; - EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 0, ref si, true); + Core.Win32.SetScrollInfo(Hwnd, 0, ref si, true); } // Sincronizar con MasterRouter para dibujado @@ -381,7 +398,7 @@ internal void RaiseResized(int w, int h) if (_materializedChildren is not null && _contentModel is not null && _materializedChildren.Count > 0) { - var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + var engine = new Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); engine.Execute(_materializedChildren, w, h, _contentModel.Spacing, _contentModel.Padding); } @@ -409,7 +426,7 @@ internal void MaterializeContent(WindowModel window, HandleRegistry registry, Ma if (availW <= 0) availW = window.Width; if (availH <= 0) availH = window.Height; - var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + var engine = new Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); engine.Execute(layoutables, availW, availH, content.Spacing, content.Padding); _materializedChildren = layoutables; diff --git a/src/EasyWindowsApplication/Share/IAlternativeWindow.cs b/src/EasyWindowsApplication/Share/IAlternativeWindow.cs index b48e794..59e5802 100644 --- a/src/EasyWindowsApplication/Share/IAlternativeWindow.cs +++ b/src/EasyWindowsApplication/Share/IAlternativeWindow.cs @@ -1,7 +1,7 @@  namespace EasyWindowsApplication.Share; -public interface IAlternativeWindow : IBaseWindow +public interface IAlternativeWindow : IBaseWindow, IInputSurface { nint OwnerHwnd { get; } string Title { get; set; } diff --git a/src/EasyWindowsApplication/Share/IInputSurface.cs b/src/EasyWindowsApplication/Share/IInputSurface.cs new file mode 100644 index 0000000..c4fa59b --- /dev/null +++ b/src/EasyWindowsApplication/Share/IInputSurface.cs @@ -0,0 +1,20 @@ +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Share; + +/// +/// Contrato unificado de suscripción a entradas (opt-in por tipo). +/// Solo los tipos que lo declaran exponen OnInput* al IntelliSense. +/// +public interface IInputSurface +{ + void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger; + + void OnInputWithSpatialPosition(Action handler) + where TTrigger : ISpatialPositionTrigger + where TCount : ITapCount; + + void OnInputWithoutSpatialPosition(Action handler) + where TTrigger : WithoutSpatialPositionTrigger; +} diff --git a/src/EasyWindowsApplication/Share/IMenuItem.cs b/src/EasyWindowsApplication/Share/IMenuItem.cs index 529de17..fe11141 100644 --- a/src/EasyWindowsApplication/Share/IMenuItem.cs +++ b/src/EasyWindowsApplication/Share/IMenuItem.cs @@ -2,7 +2,7 @@ namespace EasyWindowsApplication.Share; -public interface IMenuItem : IViewSurface, IText +public interface IMenuItem : IViewSurface, IText, IInputSurface { bool IsEnabled { get; set; } bool IsChecked { get; set; } diff --git a/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs b/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs index 5de38be..c9657f9 100644 --- a/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs +++ b/src/EasyWindowsApplication/Share/IMenuItemSeparator.cs @@ -4,6 +4,6 @@ public interface IMenuItemSeparator : IViewSurface { // Sin miembros propios en v1 (ver proposal-imenuitemseparator.md). // Name (heredado) sirve para orden + lookup bh.*. - // OnClick/Clicked (heredados) son inertes: el OS no permite - // seleccionar un separador (MF_SEPARATOR, sin ID ni comando). + // Inerte por naturaleza: el OS no permite seleccionar un separador + // (MF_SEPARATOR, sin ID ni comando), por eso no expone IInputSurface. } diff --git a/src/EasyWindowsApplication/Share/ISystemTray.cs b/src/EasyWindowsApplication/Share/ISystemTray.cs index e0469c5..495a16d 100644 --- a/src/EasyWindowsApplication/Share/ISystemTray.cs +++ b/src/EasyWindowsApplication/Share/ISystemTray.cs @@ -1,14 +1,6 @@ -using EasyWindowsApplication.Share.Input; - namespace EasyWindowsApplication.Share; -public interface ISystemTray : IViewSurface, ISystemTrayNotifications +public interface ISystemTray : IViewSurface, ISystemTrayNotifications, IInputSurface { - ISystemTray OnInputWithSpatialPosition(Action handler) - where TTrigger : ISpatialPositionTrigger; - ISystemTray OnInputWithSpatialPosition(Action handler) - where TTrigger : ISpatialPositionTrigger - where TCount : ITapCount; - ISystemTray OnInputWithoutSpatialPosition(Action handler) - where TTrigger : WithoutSpatialPositionTrigger; + } diff --git a/src/EasyWindowsApplication/Share/IViewSurface.cs b/src/EasyWindowsApplication/Share/IViewSurface.cs index 89ee3dd..cf81a47 100644 --- a/src/EasyWindowsApplication/Share/IViewSurface.cs +++ b/src/EasyWindowsApplication/Share/IViewSurface.cs @@ -17,9 +17,6 @@ public interface IViewSurface Color? BackgroundColor { get; set; } - event Action? Clicked; - void OnClick(Action handler); - void Visibility(bool visible) { if (this is IBaseWindow window) diff --git a/src/EasyWindowsApplication/Share/IWindow.cs b/src/EasyWindowsApplication/Share/IWindow.cs index ae306a6..4796123 100644 --- a/src/EasyWindowsApplication/Share/IWindow.cs +++ b/src/EasyWindowsApplication/Share/IWindow.cs @@ -1,7 +1,7 @@  namespace EasyWindowsApplication.Share; -public interface IWindow : IBaseWindow +public interface IWindow : IBaseWindow, IInputSurface { string Title { get; set; } float Width { get; set; } diff --git a/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs b/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs index 2576b5b..9e574af 100644 --- a/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs +++ b/src/EasyWindowsApplication/Share/Input/ISpatialPositionTrigger.cs @@ -6,8 +6,6 @@ public interface ISpatialPositionTrigger { } public readonly struct MainTap : ISpatialPositionTrigger { } -public readonly struct MainDoubleTap : ISpatialPositionTrigger { } - public readonly struct AlternativeTap1 : ISpatialPositionTrigger { } public readonly struct AlternativeTap2 : ISpatialPositionTrigger { } diff --git a/src/EasyWindowsApplication/Share/Input/SurfaceInputExtensions.cs b/src/EasyWindowsApplication/Share/Input/SurfaceInputExtensions.cs new file mode 100644 index 0000000..5ca68eb --- /dev/null +++ b/src/EasyWindowsApplication/Share/Input/SurfaceInputExtensions.cs @@ -0,0 +1,29 @@ +using EasyWindowsApplication.Share.Input; + +namespace EasyWindowsApplication.Share; + +/// +/// Extensiones ergonómicas sobre . +/// +public static class SurfaceInputExtensions +{ + public static void OnMainTap(this IInputSurface s, Action h) + => s.OnInputWithSpatialPosition(h); + + public static void OnMainTap(this IInputSurface s, Action h) + where TCount : ITapCount + => s.OnInputWithSpatialPosition(h); + + public static void OnAlternativeTap1(this IInputSurface s, Action h) + => s.OnInputWithSpatialPosition(h); + + public static void OnAlternativeTap2(this IInputSurface s, Action h) + => s.OnInputWithSpatialPosition(h); + + public static void OnKeyMenu(this IInputSurface s, Action h) + => s.OnInputWithoutSpatialPosition(h); + + public static void OnChord(this IInputSurface s, Action h) + where TMod : IKeyMod where TKey : IKey + => s.OnInputWithoutSpatialPosition>(h); +} diff --git a/src/EasyWindowsApplication/Share/View.cs b/src/EasyWindowsApplication/Share/View.cs index 9685146..06d8090 100644 --- a/src/EasyWindowsApplication/Share/View.cs +++ b/src/EasyWindowsApplication/Share/View.cs @@ -14,12 +14,6 @@ internal ViewBase() { } base.OnMessage(msg, handler); return (TSelf)this; } - - public new TSelf OnClick(Action handler) - { - base.OnClick(handler); - return (TSelf)this; - } } public sealed class View where T : class, IViewSurface @@ -152,12 +146,6 @@ public View ColumnSpan(int span) return this; } - public View OnClick(Action handler) - { - _configure.Add(c => c.OnClick(handler)); - return this; - } - public View Text(string text) { _configure.Add(c => { if (c is IText t) t.Text = text; }); diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs index 3993daa..fea08a2 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs @@ -16,7 +16,6 @@ public string Text } } public bool Enabled { get; set; } = true; - public void Click() => OnClick(() => { }); public void SetStyle(uint style, bool redraw = true) { } protected override (float Width, float Height) MeasureContent(float availableWidth, float availableHeight) @@ -24,7 +23,7 @@ protected override (float Width, float Height) MeasureContent(float availableWid string text = GetWindowText(); var (tw, th) = string.IsNullOrEmpty(text) ? (20, 24) : MeasureTextByGlyphs(text); var defaults = UiDefaultsProvider.Current.GetFor(); - if (defaults == null) + if (defaults is null) defaults = UiDefaultsProvider.Current.GetFor