From 79bdd8bc62c38480406210348107c72d8bc04818 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Sun, 18 Jan 2026 16:33:35 +0300 Subject: [PATCH] DragAndDrop core --- .../Abstractions/IDockCommand.cs | 8 + .../Abstractions/IDockContainer.cs | 24 + .../Abstractions/IDockContent.cs | 25 + .../Abstractions/IDockElement.cs | 25 + .../Abstractions/IDockElementDragSource.cs | 19 + .../Abstractions/IDockElementDropTarget.cs | 19 + .../Abstractions/IDragService.cs | 76 ++ Lattice.Core.Docking/Engine/DockOperations.cs | 89 ++ Lattice.Core.Docking/Engine/LayoutManager.cs | 369 ++++++++ .../Lattice.Core.Docking.csproj | 13 + Lattice.Core.Docking/Models/AutoHidePanel.cs | 114 +++ Lattice.Core.Docking/Models/DockGroup.cs | 444 ++++++++++ Lattice.Core.Docking/Models/DockLeaf.cs | 580 ++++++++++++ Lattice.Core.Docking/Models/DockPosition.cs | 13 + Lattice.Core.Docking/Models/DockSide.cs | 19 + Lattice.Core.Docking/Models/DockWindow.cs | 23 + Lattice.Core.Docking/Models/SplitDirection.cs | 12 + Lattice.Core.Docking/Models/TabPlacement.cs | 12 + .../Serialization/ILayoutSerializer.cs | 31 + .../Serialization/ISerializableLayout.cs | 19 + .../Services/ContentRegistry.cs | 158 ++++ .../DragDropServiceTests.cs | 115 +++ .../Lattice.Core.DragDrop.Tests.csproj | 27 + .../Abstractions/IAsyncDragSource.cs | 28 + .../Abstractions/IAsyncDropTarget.cs | 28 + .../Abstractions/IDragSource.cs | 64 ++ .../Abstractions/IDropTarget.cs | 55 ++ .../Enums/DragDropEffects.cs | 102 +++ Lattice.Core.DragDrop/Enums/DropPosition.cs | 14 + .../Exceptions/DragDropException.cs | 85 ++ .../Extensions/ServiceCollectionExtensions.cs | 85 ++ .../Lattice.Core.DragDrop.csproj | 20 + Lattice.Core.DragDrop/Models/DragInfo.cs | 227 +++++ Lattice.Core.DragDrop/Models/DropInfo.cs | 269 ++++++ Lattice.Core.DragDrop/README.md | 832 ++++++++++++++++++ .../Services/DragDropService.cs | 829 +++++++++++++++++ .../EventArgs/DragCancelledEventArgs.cs | 22 + .../EventArgs/DragCompletedEventArgs.cs | 35 + .../EventArgs/DragDropErrorEventArgs.cs | 32 + .../EventArgs/DragStartedEventArgs.cs | 29 + .../EventArgs/DragUpdatedEventArgs.cs | 29 + .../EventArgs/DropTargetChangedEventArgs.cs | 35 + .../Services/IDragDropService.cs | 174 ++++ .../Utilities/AsyncDragDropUtilities.cs | 713 +++++++++++++++ .../Utilities/DragDropUtilities.cs | 275 ++++++ .../Lattice.Core.Geometry.csproj | 9 + Lattice.Core.Geometry/Point.cs | 79 ++ Lattice.Core.Geometry/Rect.cs | 153 ++++ Lattice.Core.Geometry/Size.cs | 84 ++ Lattice.Core/Abstractions/IContextService.cs | 27 - .../Abstractions/IDockableComponent.cs | 33 - Lattice.Core/Abstractions/ILayoutElement.cs | 42 - Lattice.Core/Abstractions/ILayoutService.cs | 40 - .../Abstractions/INotificationService.cs | 17 - Lattice.Core/Lattice.Core.csproj | 25 - Lattice.Core/Models/ActionDefinition.cs | 37 - Lattice.Core/Models/ContentNode.cs | 29 - Lattice.Core/Models/Enums/DockDirection.cs | 11 - .../Models/Enums/NotificationSeverity.cs | 8 - Lattice.Core/Models/Enums/SplitOrientation.cs | 13 - Lattice.Core/Models/LayoutNode.cs | 35 - Lattice.Core/Models/NotificationEventArgs.cs | 5 - Lattice.Core/Models/SplitContainerNode.cs | 38 - Lattice.Core/Models/WorkspaceSnapshot.cs | 12 - .../Persistence/LayoutJsonConverter.cs | 31 - Lattice.Core/README.md | 54 -- Lattice.Core/Services/ContextService.cs | 39 - Lattice.Core/Services/LayoutService.cs | 228 ----- Lattice.Core/Services/NotificationService.cs | 19 - Lattice.IDE/App.xaml | 19 + Lattice.IDE/App.xaml.cs | 37 + .../Assets/LockScreenLogo.scale-200.png | Bin 0 -> 432 bytes Lattice.IDE/Assets/SplashScreen.scale-200.png | Bin 0 -> 5372 bytes .../Assets/Square150x150Logo.scale-200.png | Bin 0 -> 1755 bytes .../Assets/Square44x44Logo.scale-200.png | Bin 0 -> 637 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 283 bytes Lattice.IDE/Assets/StoreLogo.png | Bin 0 -> 456 bytes .../Assets/Wide310x150Logo.scale-200.png | Bin 0 -> 2097 bytes Lattice.IDE/Controls/EditorView.xaml | 58 ++ Lattice.IDE/Controls/EditorView.xaml.cs | 28 + .../Controls/SolutionExplorerView.xaml | 40 + .../Controls/SolutionExplorerView.xaml.cs | 28 + Lattice.IDE/Lattice.IDE.csproj | 81 ++ Lattice.IDE/Layout/DemoContent.cs | 28 + Lattice.IDE/MainWindow.xaml | 29 + Lattice.IDE/MainWindow.xaml.cs | 64 ++ Lattice.IDE/Package.appxmanifest | 51 ++ Lattice.IDE/Properties/launchSettings.json | 10 + Lattice.IDE/app.manifest | 19 + .../Controls/WinUIGroupControl.cs | 30 + .../Controls/WinUIItemControl.cs | 134 +++ .../Controls/WinUILayoutHost.cs | 134 +++ .../Controls/WinUISplitControl.cs | 85 ++ .../Docking/DockOverlay.cs | 83 ++ .../Docking/DockOverlayHost.cs | 47 + .../Docking/DockZoneHitTester.cs | 78 ++ .../Docking/IWinUIVisual.cs | 15 + .../Helpers/LayoutHostExtensions.cs | 118 +++ .../Lattice.Layout.UI.WinUI.csproj | 18 + .../Rendering/WinUIVisualFactory.cs | 30 + .../Visuals/WinUIGroupVisual.cs | 81 ++ .../Visuals/WinUIItemVisual.cs | 51 ++ .../Visuals/WinUISplitVisual.cs | 82 ++ .../JsonLayoutSerializer.cs | 147 ++++ .../JsonSerializerOptions.cs | 48 + .../Lattice.Serialization.Docking.Json.csproj | 12 + .../DTO/AutoHidePanelDto.cs | 37 + .../DTO/ContentReferenceDto.cs | 32 + .../DTO/ElementDto.cs | 37 + Lattice.Serialization.Docking/DTO/GroupDto.cs | 27 + .../DTO/LayoutDto.cs | 47 + Lattice.Serialization.Docking/DTO/LeafDto.cs | 22 + .../DTO/WindowDto.cs | 52 ++ .../ILayoutSerializer.cs | 103 +++ .../ISerializableContent.cs | 19 + .../Lattice.Serialization.Docking.csproj | 12 + .../LayoutConverter.cs | 317 +++++++ .../Controls/LatticeStudioShell.xaml | 64 -- .../Controls/LatticeStudioShell.xaml.cs | 144 --- .../Controls/LatticeStudioWindow.xaml | 18 - .../Controls/LatticeStudioWindow.xaml.cs | 116 --- Lattice.Studio/Lattice.Studio.csproj | 45 - Lattice.Studio/README.md | 182 ---- Lattice.Studio/Themes/StudioThemes.xaml | 22 - .../Lattice.Themes.Core.csproj | 19 + Lattice.Themes.Core/LatticeTokens.cs | 231 +++++ Lattice.Themes.Core/ThemeChangedEventArgs.cs | 13 + Lattice.Themes.Core/ThemeDictionary.cs | 7 + Lattice.Themes.Core/ThemeManager.cs | 293 ++++++ Lattice.Themes.Core/ThemePack.cs | 111 +++ Lattice.Themes.Core/WindowTracker.cs | 18 + Lattice.Themes.Fluent/Brushes.xaml | 74 ++ Lattice.Themes.Fluent/Colors/Dark.xaml | 48 + Lattice.Themes.Fluent/Colors/Light.xaml | 48 + Lattice.Themes.Fluent/FluentThemePack.cs | 45 + Lattice.Themes.Fluent/Geometry.xaml | 40 + .../Lattice.Themes.Fluent.csproj | 50 ++ Lattice.Themes.Fluent/Main.xaml | 60 ++ Lattice.Themes.Fluent/Tokens.xaml | 191 ++++ Lattice.Themes.Fluent/Typography.xaml | 50 ++ Lattice.Themes.VS2026/Brushes.xaml | 44 + Lattice.Themes.VS2026/Colors/Dark.xaml | 50 ++ Lattice.Themes.VS2026/Colors/Light.xaml | 50 ++ Lattice.Themes.VS2026/Geometry.xaml | 40 + .../Lattice.Themes.VS2026.csproj | 55 ++ Lattice.Themes.VS2026/Main.xaml | 49 ++ Lattice.Themes.VS2026/Tokens.xaml | 190 ++++ Lattice.Themes.VS2026/Typography.xaml | 43 + Lattice.Themes.VS2026/VS2026ThemePack.cs | 41 + .../Controls/LatticeDockGroup.cs | 368 ++++++++ .../Controls/LatticeDockHost.cs | 611 +++++++++++++ .../Controls/LatticeDockLeaf.cs | 40 + .../Controls/LatticeSplitter.cs | 51 ++ .../Controls/LatticeTabControl.cs | 688 +++++++++++++++ .../Converters/DockTemplateSelector.cs | 27 + .../Factories/WinUIDockControlFactory.cs | 96 ++ .../Lattice.UI.Docking.WinUI.csproj | 22 + .../Services/WinUIDockContextManager.cs | 116 +++ .../Services/WinUIDockUIService.cs | 167 ++++ .../Services/WinUIDragDropService.cs | 534 +++++++++++ Lattice.UI.Docking.WinUI/Themes/Generic.xaml | 195 ++++ .../Abstractions/IAutoHidePanelControl.cs | 97 ++ .../Abstractions/IDockCommand.cs | 50 ++ .../Abstractions/IDockContextManager.cs | 43 + .../Abstractions/IDockControl.cs | 103 +++ .../Abstractions/IDockDragDropService.cs | 256 ++++++ .../Abstractions/IDockGroupControl.cs | 108 +++ Lattice.UI.Docking/Abstractions/IDockHost.cs | 235 +++++ .../Abstractions/IDockLeafControl.cs | 180 ++++ Lattice.UI.Docking/Abstractions/IDockTheme.cs | 108 +++ .../Abstractions/IDockUIService.cs | 114 +++ .../Abstractions/IFloatingWindowControl.cs | 110 +++ .../Commands/DockCommandBase.cs | 115 +++ .../Factories/DockControlFactoryBase.cs | 65 ++ .../Factories/IDockControlFactory.cs | 101 +++ Lattice.UI.Docking/Lattice.UI.Docking.csproj | 16 + Lattice.UI.Docking/LatticeUIFramework.cs | 439 +++++++++ Lattice.UI.Docking/Models/UiDragDropModels.cs | 148 ++++ .../Services/DockContextManagerBase.cs | 96 ++ .../Services/DockDragDropService.cs | 417 +++++++++ .../Services/DockUIServiceBase.cs | 113 +++ Lattice.UI.Docking/Utilities/DockUtilities.cs | 93 ++ .../Behaviors/WinUIDragSourceBehavior.cs | 429 +++++++++ .../Behaviors/WinUIDropTargetBehavior.cs | 402 +++++++++ .../Controls/DragAdorner.cs | 213 +++++ .../Controls/DragDropOverlay.cs | 156 ++++ .../Controls/DropPreviewAdorner.cs | 141 +++ .../Extensions/DragDropExtensions.cs | 327 +++++++ .../Extensions/ThemeExtensions.cs | 120 +++ .../Helpers/ResourceHelper.cs | 102 +++ .../WinUIDragDropIntegrationService.cs | 487 ++++++++++ .../Lattice.UI.DragDrop.WinUI.csproj | 25 + Lattice.UI.DragDrop.WinUI/README.md | 547 ++++++++++++ .../Services/DragDropConfigurationService.cs | 259 ++++++ .../WinUIDragDropIntegrationService.cs | 132 +++ .../Services/WinUIDragVisualProvider.cs | 87 ++ .../Themes/DragAdorner.xaml | 44 + .../Themes/DragDropStyles.xaml | 153 ++++ .../Themes/DropPreviewAdorner.xaml | 52 ++ Lattice.UI.DragDrop.WinUI/Themes/Generic.xaml | 19 + .../Abstractions/IDragDropHost.cs | 41 + .../Abstractions/IDragVisualProvider.cs | 31 + .../Abstractions/IDropVisualAdorner.cs | 28 + .../Behaviors/DragSourceBehaviorBase.cs | 256 ++++++ .../Behaviors/DropTargetBehaviorBase.cs | 214 +++++ .../Extensions/ServiceCollectionExtensions.cs | 52 ++ .../Lattice.UI.DragDrop.csproj | 20 + Lattice.UI.DragDrop/README.md | 136 +++ .../Controls/LatticeContextualToolbar.cs | 49 -- Lattice.UI/Controls/LatticeDockHost.cs | 106 --- Lattice.UI/Controls/LatticeFloatingWindow.cs | 49 -- Lattice.UI/Controls/LatticePane.cs | 83 -- Lattice.UI/Controls/LatticeSplitter.cs | 99 --- Lattice.UI/Controls/LatticeTabStrip.cs | 43 - Lattice.UI/DragDrop/DockTabHandler.cs | 145 --- Lattice.UI/Lattice.UI.csproj | 36 - Lattice.UI/Primitives/DockAnchorOverlay.cs | 71 -- Lattice.UI/Primitives/LatticeIcon.cs | 16 - Lattice.UI/Primitives/LayoutPanel.cs | 121 --- Lattice.UI/README.md | 56 -- Lattice.UI/Services/VisualTreeService.cs | 52 -- Lattice.UI/Themes/Generic.xaml | 17 - .../Themes/Styles/DockAnchorOverlay.xaml | 40 - Lattice.UI/Themes/Styles/LatticeDockHost.xaml | 33 - .../Themes/Styles/LatticeNotification.xaml | 15 - Lattice.UI/Themes/Styles/LatticePane.xaml | 70 -- Lattice.UI/Themes/Styles/LatticeSplitter.xaml | 30 - Lattice.UI/Themes/Styles/SharedResources.xaml | 26 - Lattice.slnx | 31 +- 229 files changed, 21214 insertions(+), 2494 deletions(-) create mode 100644 Lattice.Core.Docking/Abstractions/IDockCommand.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDockContainer.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDockContent.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDockElement.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs create mode 100644 Lattice.Core.Docking/Abstractions/IDragService.cs create mode 100644 Lattice.Core.Docking/Engine/DockOperations.cs create mode 100644 Lattice.Core.Docking/Engine/LayoutManager.cs create mode 100644 Lattice.Core.Docking/Lattice.Core.Docking.csproj create mode 100644 Lattice.Core.Docking/Models/AutoHidePanel.cs create mode 100644 Lattice.Core.Docking/Models/DockGroup.cs create mode 100644 Lattice.Core.Docking/Models/DockLeaf.cs create mode 100644 Lattice.Core.Docking/Models/DockPosition.cs create mode 100644 Lattice.Core.Docking/Models/DockSide.cs create mode 100644 Lattice.Core.Docking/Models/DockWindow.cs create mode 100644 Lattice.Core.Docking/Models/SplitDirection.cs create mode 100644 Lattice.Core.Docking/Models/TabPlacement.cs create mode 100644 Lattice.Core.Docking/Serialization/ILayoutSerializer.cs create mode 100644 Lattice.Core.Docking/Serialization/ISerializableLayout.cs create mode 100644 Lattice.Core.Docking/Services/ContentRegistry.cs create mode 100644 Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs create mode 100644 Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj create mode 100644 Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs create mode 100644 Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs create mode 100644 Lattice.Core.DragDrop/Abstractions/IDragSource.cs create mode 100644 Lattice.Core.DragDrop/Abstractions/IDropTarget.cs create mode 100644 Lattice.Core.DragDrop/Enums/DragDropEffects.cs create mode 100644 Lattice.Core.DragDrop/Enums/DropPosition.cs create mode 100644 Lattice.Core.DragDrop/Exceptions/DragDropException.cs create mode 100644 Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs create mode 100644 Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj create mode 100644 Lattice.Core.DragDrop/Models/DragInfo.cs create mode 100644 Lattice.Core.DragDrop/Models/DropInfo.cs create mode 100644 Lattice.Core.DragDrop/README.md create mode 100644 Lattice.Core.DragDrop/Services/DragDropService.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DragCancelledEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DragCompletedEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DragDropErrorEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DragStartedEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DragUpdatedEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/EventArgs/DropTargetChangedEventArgs.cs create mode 100644 Lattice.Core.DragDrop/Services/IDragDropService.cs create mode 100644 Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs create mode 100644 Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs create mode 100644 Lattice.Core.Geometry/Lattice.Core.Geometry.csproj create mode 100644 Lattice.Core.Geometry/Point.cs create mode 100644 Lattice.Core.Geometry/Rect.cs create mode 100644 Lattice.Core.Geometry/Size.cs delete mode 100644 Lattice.Core/Abstractions/IContextService.cs delete mode 100644 Lattice.Core/Abstractions/IDockableComponent.cs delete mode 100644 Lattice.Core/Abstractions/ILayoutElement.cs delete mode 100644 Lattice.Core/Abstractions/ILayoutService.cs delete mode 100644 Lattice.Core/Abstractions/INotificationService.cs delete mode 100644 Lattice.Core/Lattice.Core.csproj delete mode 100644 Lattice.Core/Models/ActionDefinition.cs delete mode 100644 Lattice.Core/Models/ContentNode.cs delete mode 100644 Lattice.Core/Models/Enums/DockDirection.cs delete mode 100644 Lattice.Core/Models/Enums/NotificationSeverity.cs delete mode 100644 Lattice.Core/Models/Enums/SplitOrientation.cs delete mode 100644 Lattice.Core/Models/LayoutNode.cs delete mode 100644 Lattice.Core/Models/NotificationEventArgs.cs delete mode 100644 Lattice.Core/Models/SplitContainerNode.cs delete mode 100644 Lattice.Core/Models/WorkspaceSnapshot.cs delete mode 100644 Lattice.Core/Persistence/LayoutJsonConverter.cs delete mode 100644 Lattice.Core/README.md delete mode 100644 Lattice.Core/Services/ContextService.cs delete mode 100644 Lattice.Core/Services/LayoutService.cs delete mode 100644 Lattice.Core/Services/NotificationService.cs create mode 100644 Lattice.IDE/App.xaml create mode 100644 Lattice.IDE/App.xaml.cs create mode 100644 Lattice.IDE/Assets/LockScreenLogo.scale-200.png create mode 100644 Lattice.IDE/Assets/SplashScreen.scale-200.png create mode 100644 Lattice.IDE/Assets/Square150x150Logo.scale-200.png create mode 100644 Lattice.IDE/Assets/Square44x44Logo.scale-200.png create mode 100644 Lattice.IDE/Assets/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 Lattice.IDE/Assets/StoreLogo.png create mode 100644 Lattice.IDE/Assets/Wide310x150Logo.scale-200.png create mode 100644 Lattice.IDE/Controls/EditorView.xaml create mode 100644 Lattice.IDE/Controls/EditorView.xaml.cs create mode 100644 Lattice.IDE/Controls/SolutionExplorerView.xaml create mode 100644 Lattice.IDE/Controls/SolutionExplorerView.xaml.cs create mode 100644 Lattice.IDE/Lattice.IDE.csproj create mode 100644 Lattice.IDE/Layout/DemoContent.cs create mode 100644 Lattice.IDE/MainWindow.xaml create mode 100644 Lattice.IDE/MainWindow.xaml.cs create mode 100644 Lattice.IDE/Package.appxmanifest create mode 100644 Lattice.IDE/Properties/launchSettings.json create mode 100644 Lattice.IDE/app.manifest create mode 100644 Lattice.Layout.UI.WinUI/Controls/WinUIGroupControl.cs create mode 100644 Lattice.Layout.UI.WinUI/Controls/WinUIItemControl.cs create mode 100644 Lattice.Layout.UI.WinUI/Controls/WinUILayoutHost.cs create mode 100644 Lattice.Layout.UI.WinUI/Controls/WinUISplitControl.cs create mode 100644 Lattice.Layout.UI.WinUI/Docking/DockOverlay.cs create mode 100644 Lattice.Layout.UI.WinUI/Docking/DockOverlayHost.cs create mode 100644 Lattice.Layout.UI.WinUI/Docking/DockZoneHitTester.cs create mode 100644 Lattice.Layout.UI.WinUI/Docking/IWinUIVisual.cs create mode 100644 Lattice.Layout.UI.WinUI/Helpers/LayoutHostExtensions.cs create mode 100644 Lattice.Layout.UI.WinUI/Lattice.Layout.UI.WinUI.csproj create mode 100644 Lattice.Layout.UI.WinUI/Rendering/WinUIVisualFactory.cs create mode 100644 Lattice.Layout.UI.WinUI/Visuals/WinUIGroupVisual.cs create mode 100644 Lattice.Layout.UI.WinUI/Visuals/WinUIItemVisual.cs create mode 100644 Lattice.Layout.UI.WinUI/Visuals/WinUISplitVisual.cs create mode 100644 Lattice.Serialization.Docking.Json/JsonLayoutSerializer.cs create mode 100644 Lattice.Serialization.Docking.Json/JsonSerializerOptions.cs create mode 100644 Lattice.Serialization.Docking.Json/Lattice.Serialization.Docking.Json.csproj create mode 100644 Lattice.Serialization.Docking/DTO/AutoHidePanelDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/ContentReferenceDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/ElementDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/GroupDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/LayoutDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/LeafDto.cs create mode 100644 Lattice.Serialization.Docking/DTO/WindowDto.cs create mode 100644 Lattice.Serialization.Docking/ILayoutSerializer.cs create mode 100644 Lattice.Serialization.Docking/ISerializableContent.cs create mode 100644 Lattice.Serialization.Docking/Lattice.Serialization.Docking.csproj create mode 100644 Lattice.Serialization.Docking/LayoutConverter.cs delete mode 100644 Lattice.Studio/Controls/LatticeStudioShell.xaml delete mode 100644 Lattice.Studio/Controls/LatticeStudioShell.xaml.cs delete mode 100644 Lattice.Studio/Controls/LatticeStudioWindow.xaml delete mode 100644 Lattice.Studio/Controls/LatticeStudioWindow.xaml.cs delete mode 100644 Lattice.Studio/Lattice.Studio.csproj delete mode 100644 Lattice.Studio/README.md delete mode 100644 Lattice.Studio/Themes/StudioThemes.xaml create mode 100644 Lattice.Themes.Core/Lattice.Themes.Core.csproj create mode 100644 Lattice.Themes.Core/LatticeTokens.cs create mode 100644 Lattice.Themes.Core/ThemeChangedEventArgs.cs create mode 100644 Lattice.Themes.Core/ThemeDictionary.cs create mode 100644 Lattice.Themes.Core/ThemeManager.cs create mode 100644 Lattice.Themes.Core/ThemePack.cs create mode 100644 Lattice.Themes.Core/WindowTracker.cs create mode 100644 Lattice.Themes.Fluent/Brushes.xaml create mode 100644 Lattice.Themes.Fluent/Colors/Dark.xaml create mode 100644 Lattice.Themes.Fluent/Colors/Light.xaml create mode 100644 Lattice.Themes.Fluent/FluentThemePack.cs create mode 100644 Lattice.Themes.Fluent/Geometry.xaml create mode 100644 Lattice.Themes.Fluent/Lattice.Themes.Fluent.csproj create mode 100644 Lattice.Themes.Fluent/Main.xaml create mode 100644 Lattice.Themes.Fluent/Tokens.xaml create mode 100644 Lattice.Themes.Fluent/Typography.xaml create mode 100644 Lattice.Themes.VS2026/Brushes.xaml create mode 100644 Lattice.Themes.VS2026/Colors/Dark.xaml create mode 100644 Lattice.Themes.VS2026/Colors/Light.xaml create mode 100644 Lattice.Themes.VS2026/Geometry.xaml create mode 100644 Lattice.Themes.VS2026/Lattice.Themes.VS2026.csproj create mode 100644 Lattice.Themes.VS2026/Main.xaml create mode 100644 Lattice.Themes.VS2026/Tokens.xaml create mode 100644 Lattice.Themes.VS2026/Typography.xaml create mode 100644 Lattice.Themes.VS2026/VS2026ThemePack.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs create mode 100644 Lattice.UI.Docking.WinUI/Converters/DockTemplateSelector.cs create mode 100644 Lattice.UI.Docking.WinUI/Factories/WinUIDockControlFactory.cs create mode 100644 Lattice.UI.Docking.WinUI/Lattice.UI.Docking.WinUI.csproj create mode 100644 Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs create mode 100644 Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs create mode 100644 Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs create mode 100644 Lattice.UI.Docking.WinUI/Themes/Generic.xaml create mode 100644 Lattice.UI.Docking/Abstractions/IAutoHidePanelControl.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockCommand.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockContextManager.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockControl.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockDragDropService.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockGroupControl.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockHost.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockLeafControl.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockTheme.cs create mode 100644 Lattice.UI.Docking/Abstractions/IDockUIService.cs create mode 100644 Lattice.UI.Docking/Abstractions/IFloatingWindowControl.cs create mode 100644 Lattice.UI.Docking/Commands/DockCommandBase.cs create mode 100644 Lattice.UI.Docking/Factories/DockControlFactoryBase.cs create mode 100644 Lattice.UI.Docking/Factories/IDockControlFactory.cs create mode 100644 Lattice.UI.Docking/Lattice.UI.Docking.csproj create mode 100644 Lattice.UI.Docking/LatticeUIFramework.cs create mode 100644 Lattice.UI.Docking/Models/UiDragDropModels.cs create mode 100644 Lattice.UI.Docking/Services/DockContextManagerBase.cs create mode 100644 Lattice.UI.Docking/Services/DockDragDropService.cs create mode 100644 Lattice.UI.Docking/Services/DockUIServiceBase.cs create mode 100644 Lattice.UI.Docking/Utilities/DockUtilities.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Behaviors/WinUIDragSourceBehavior.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Behaviors/WinUIDropTargetBehavior.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Controls/DragAdorner.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Controls/DragDropOverlay.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Controls/DropPreviewAdorner.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Extensions/DragDropExtensions.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Extensions/ThemeExtensions.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Helpers/ResourceHelper.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Integration/WinUIDragDropIntegrationService.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Lattice.UI.DragDrop.WinUI.csproj create mode 100644 Lattice.UI.DragDrop.WinUI/README.md create mode 100644 Lattice.UI.DragDrop.WinUI/Services/DragDropConfigurationService.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Services/WinUIDragDropIntegrationService.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Services/WinUIDragVisualProvider.cs create mode 100644 Lattice.UI.DragDrop.WinUI/Themes/DragAdorner.xaml create mode 100644 Lattice.UI.DragDrop.WinUI/Themes/DragDropStyles.xaml create mode 100644 Lattice.UI.DragDrop.WinUI/Themes/DropPreviewAdorner.xaml create mode 100644 Lattice.UI.DragDrop.WinUI/Themes/Generic.xaml create mode 100644 Lattice.UI.DragDrop/Abstractions/IDragDropHost.cs create mode 100644 Lattice.UI.DragDrop/Abstractions/IDragVisualProvider.cs create mode 100644 Lattice.UI.DragDrop/Abstractions/IDropVisualAdorner.cs create mode 100644 Lattice.UI.DragDrop/Behaviors/DragSourceBehaviorBase.cs create mode 100644 Lattice.UI.DragDrop/Behaviors/DropTargetBehaviorBase.cs create mode 100644 Lattice.UI.DragDrop/Extensions/ServiceCollectionExtensions.cs create mode 100644 Lattice.UI.DragDrop/Lattice.UI.DragDrop.csproj create mode 100644 Lattice.UI.DragDrop/README.md delete mode 100644 Lattice.UI/Controls/LatticeContextualToolbar.cs delete mode 100644 Lattice.UI/Controls/LatticeDockHost.cs delete mode 100644 Lattice.UI/Controls/LatticeFloatingWindow.cs delete mode 100644 Lattice.UI/Controls/LatticePane.cs delete mode 100644 Lattice.UI/Controls/LatticeSplitter.cs delete mode 100644 Lattice.UI/Controls/LatticeTabStrip.cs delete mode 100644 Lattice.UI/DragDrop/DockTabHandler.cs delete mode 100644 Lattice.UI/Lattice.UI.csproj delete mode 100644 Lattice.UI/Primitives/DockAnchorOverlay.cs delete mode 100644 Lattice.UI/Primitives/LatticeIcon.cs delete mode 100644 Lattice.UI/Primitives/LayoutPanel.cs delete mode 100644 Lattice.UI/README.md delete mode 100644 Lattice.UI/Services/VisualTreeService.cs delete mode 100644 Lattice.UI/Themes/Generic.xaml delete mode 100644 Lattice.UI/Themes/Styles/DockAnchorOverlay.xaml delete mode 100644 Lattice.UI/Themes/Styles/LatticeDockHost.xaml delete mode 100644 Lattice.UI/Themes/Styles/LatticeNotification.xaml delete mode 100644 Lattice.UI/Themes/Styles/LatticePane.xaml delete mode 100644 Lattice.UI/Themes/Styles/LatticeSplitter.xaml delete mode 100644 Lattice.UI/Themes/Styles/SharedResources.xaml diff --git a/Lattice.Core.Docking/Abstractions/IDockCommand.cs b/Lattice.Core.Docking/Abstractions/IDockCommand.cs new file mode 100644 index 0000000..e7b064f --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockCommand.cs @@ -0,0 +1,8 @@ +namespace Lattice.Core.Docking.Abstractions; + +public interface IDockCommand : System.Windows.Input.ICommand +{ + string Name { get; } + string Icon { get; } + string GestureText { get; } +} diff --git a/Lattice.Core.Docking/Abstractions/IDockContainer.cs b/Lattice.Core.Docking/Abstractions/IDockContainer.cs new file mode 100644 index 0000000..404924d --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockContainer.cs @@ -0,0 +1,24 @@ +using Lattice.Core.Docking.Models; + +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Интерфейс для элементов (листьев дерева), которые физически содержат внутри себя коллекцию вкладок. +/// +public interface IDockContainer : IDockElement +{ + /// Список вкладок, находящихся в данном контейнере. + IList Children { get; } + + /// Ссылка на текущую выбранную и отображаемую вкладку. + IDockContent? ActiveContent { get; set; } + + /// Добавляет контент в контейнер и делает его активным. + void AddContent(IDockContent content); + + /// Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета. + void RemoveContent(IDockContent content); + + /// Положение вкладок в интерфейсе. + TabPlacement TabPlacement { get; set; } +} diff --git a/Lattice.Core.Docking/Abstractions/IDockContent.cs b/Lattice.Core.Docking/Abstractions/IDockContent.cs new file mode 100644 index 0000000..cd719d5 --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockContent.cs @@ -0,0 +1,25 @@ +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer. +/// +public interface IDockContent +{ + /// Уникальный идентификатор контента (например, путь к файлу или ID инструмента). + string Id { get; } + + /// Заголовок, отображаемый пользователю в интерфейсе (на вкладке). + string Title { get; } + + /// + /// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement). + /// Lattice просто отображает этот объект в теле вкладки. + /// + object View { get; set; } + + /// Флаг, определяющий доступность кнопки закрытия для пользователя. + bool CanClose { get; } + + /// Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено. + bool OnClosing(); +} diff --git a/Lattice.Core.Docking/Abstractions/IDockElement.cs b/Lattice.Core.Docking/Abstractions/IDockElement.cs new file mode 100644 index 0000000..9131a85 --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockElement.cs @@ -0,0 +1,25 @@ +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice. +/// +public interface IDockElement +{ + /// Уникальный идентификатор элемента. + string Id { get; } + + /// Родительский элемент в иерархии. Если null — элемент является корневым. + IDockElement? Parent { get; set; } + + /// Желаемая ширина элемента в относительных или абсолютных единицах. + double Width { get; set; } + + /// Желаемая высота элемента в относительных или абсолютных единицах. + double Height { get; set; } + + /// Минимально допустимая ширина, при которой элемент сохраняет функциональность. + double MinWidth { get; } + + /// Минимально допустимая высота, при которой элемент сохраняет функциональность. + double MinHeight { get; } +} diff --git a/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs b/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs new file mode 100644 index 0000000..42ac214 --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs @@ -0,0 +1,19 @@ +using Lattice.Core.DragDrop.Abstractions; + +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Расширяет интерфейс элемента док-системы для поддержки операций перетаскивания. +/// +public interface IDockElementDragSource : IDockElement, IDragSource +{ + /// + /// Получает или устанавливает признак того, что элемент можно перетаскивать. + /// + bool CanDrag { get; set; } + + /// + /// Получает тип данных для перетаскивания этого элемента. + /// + string DragDataType { get; } +} diff --git a/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs b/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs new file mode 100644 index 0000000..642a810 --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs @@ -0,0 +1,19 @@ +using Lattice.Core.DragDrop.Abstractions; + +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Расширяет интерфейс элемента док-системы для возможности быть целью сброса. +/// +public interface IDockElementDropTarget : IDockElement, IDropTarget +{ + /// + /// Получает или устанавливает признак того, что элемент может принимать сброс. + /// + bool CanDrop { get; set; } + + /// + /// Получает типы данных, которые может принимать элемент. + /// + IEnumerable AcceptableDropTypes { get; } +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Abstractions/IDragService.cs b/Lattice.Core.Docking/Abstractions/IDragService.cs new file mode 100644 index 0000000..1fbc4bc --- /dev/null +++ b/Lattice.Core.Docking/Abstractions/IDragService.cs @@ -0,0 +1,76 @@ +using Lattice.Core.Docking.Models; +using Lattice.Core.Geometry; + +namespace Lattice.Core.Docking.Abstractions; + +/// +/// Предоставляет абстракцию для операции перетаскивания в док-системе. +/// Эта абстракция позволяет отделить логику перетаскивания от конкретной UI-платформы. +/// +public interface IDragService +{ + /// + /// Начинает операцию перетаскивания указанного элемента. + /// + /// Элемент для перетаскивания. + /// Визуальная обратная связь (зависит от платформы). + void StartDrag(IDockElement element, object? visualFeedback = null); + + /// + /// Обновляет позицию перетаскивания. + /// + /// Координата X. + /// Координата Y. + void UpdateDrag(double x, double y); + + /// + /// Завершает операцию перетаскивания. + /// + /// Координата X завершения. + /// Координата Y завершения. + void EndDrag(double x, double y); + + /// + /// Отменяет операцию перетаскивания. + /// + void CancelDrag(); +} + +/// +/// Представляет область для сброса при операции перетаскивания. +/// +public class DropArea +{ + /// + /// Целевой элемент для сброса. + /// + public IDockElement Target { get; set; } + + /// + /// Позиция сброса относительно цели. + /// + public DockPosition Position { get; set; } + + /// + /// Границы области в экранных координатах. + /// + public Rect Bounds { get; set; } + + /// + /// Видимость области (для анимации). + /// + public double Visibility { get; set; } = 0.0; + + /// + /// Инициализирует новый экземпляр области сброса. + /// + /// Целевой элемент. + /// Позиция сброса. + /// Границы области. + public DropArea(IDockElement target, DockPosition position, Rect bounds) + { + Target = target; + Position = position; + Bounds = bounds; + } +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Engine/DockOperations.cs b/Lattice.Core.Docking/Engine/DockOperations.cs new file mode 100644 index 0000000..4c6a5c5 --- /dev/null +++ b/Lattice.Core.Docking/Engine/DockOperations.cs @@ -0,0 +1,89 @@ +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.Docking.Models; + +namespace Lattice.Core.Docking.Engine; + +/// +/// Статический движок для манипуляции иерархией дерева компоновки. +/// Содержит чистые алгоритмы трансформации графа. +/// +public static class DockOperations +{ + /// + /// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком, + /// она удаляется, а ребенок занимает её место. + /// + /// Элемент для удаления. + /// Текущий корень дерева. + /// Новый корень дерева после оптимизации. + public static IDockElement? Remove(IDockElement element, IDockElement root) + { + if (element == root) return null; + + var parent = element.Parent as DockGroup; + if (parent == null) return root; + + // Определяем "выжившего" соседа + var sibling = (parent.First == element) ? parent.Second : parent.First; + var grandParent = parent.Parent as DockGroup; + + if (grandParent != null) + { + // Переподключаем соседа напрямую к дедушке + if (grandParent.First == parent) grandParent.First = sibling; + else grandParent.Second = sibling; + + sibling.Parent = grandParent; + return root; + } + + // Если дедушки нет, сосед становится новым корнем + sibling.Parent = null; + return sibling; + } + + /// + /// Вставляет элемент в дерево, создавая новую группу разделения или объединяя контент. + /// + public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement root) + { + // Случай 1: Объединение вкладок в центре + if (pos == DockPosition.Center) + { + if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer) + { + var items = new List(sourceContainer.Children); + foreach (var item in items) + { + sourceContainer.RemoveContent(item); + targetContainer.AddContent(item); + } + } + return root; + } + + // Случай 2: Разделение (Split) + var direction = (pos == DockPosition.Left || pos == DockPosition.Right) + ? SplitDirection.Horizontal : SplitDirection.Vertical; + + bool sourceIsFirst = (pos == DockPosition.Left || pos == DockPosition.Top); + + var oldParent = target.Parent; + + // Создаем новую группу. Источник и цель делят пространство 50/50 + var newGroup = sourceIsFirst + ? new DockGroup(source, target, direction) { SplitRatio = 0.5 } + : new DockGroup(target, source, direction) { SplitRatio = 0.5 }; + + if (oldParent is DockGroup gp) + { + if (gp.First == target) gp.First = newGroup; + else gp.Second = newGroup; + newGroup.Parent = gp; + return root; + } + + newGroup.Parent = null; + return newGroup; // Новая группа стала корнем + } +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Engine/LayoutManager.cs b/Lattice.Core.Docking/Engine/LayoutManager.cs new file mode 100644 index 0000000..f652021 --- /dev/null +++ b/Lattice.Core.Docking/Engine/LayoutManager.cs @@ -0,0 +1,369 @@ +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.Docking.Models; +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")] + +namespace Lattice.Core.Docking.Engine; + +/// +/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов +/// и расширенные операции управления макетом. +/// +/// +/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки, +/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом. +/// +public class LayoutManager +{ + private readonly ObservableCollection _autoHidePanels = new(); + + /// + /// Корневой элемент главного окна IDE. + /// + public IDockElement? Root { get; internal set; } + + /// + /// Список активных плавающих окон. + /// + public List FloatingWindows { get; } = new(); + + /// + /// Коллекция автоскрываемых панелей. + /// + public ReadOnlyObservableCollection AutoHidePanels { get; } + + /// + /// Реестр типов контента (опционально). + /// + public Services.ContentRegistry? ContentRegistry { get; set; } + + /// + /// Уведомляет UI, что структура дерева изменилась. + /// + public event Action? LayoutUpdated; + + /// + /// Уведомляет об изменении в коллекции автоскрываемых панелей. + /// + public event EventHandler? AutoHidePanelsChanged; + + /// + /// Событие, возникающее при операции перетаскивания элемента. + /// + public event EventHandler? DragDropOperation; + + /// + /// Инициализирует новый экземпляр менеджера макета. + /// + public LayoutManager() + { + AutoHidePanels = new ReadOnlyObservableCollection(_autoHidePanels); + } + + /// + /// Добавляет автоскрываемую панель. + /// + /// Содержимое панели. + /// Сторона для прикрепления. + /// Созданная автоскрываемая панель. + public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side) + { + var panel = new AutoHidePanel(content, side); + _autoHidePanels.Add(panel); + AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); + return panel; + } + + /// + /// Удаляет автоскрываемую панель. + /// + /// Панель для удаления. + public void RemoveAutoHidePanel(AutoHidePanel panel) + { + if (_autoHidePanels.Remove(panel)) + { + AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Создает документ из зарегистрированного типа контента. + /// + /// Идентификатор типа контента. + /// Уникальный идентификатор документа. + /// Созданный контент или null, если ContentRegistry не установлен. + public IDockContent? CreateDocument(string contentTypeId, string id) + { + if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId)) + return null; + + return ContentRegistry.CreateContent(contentTypeId, id); + } + + /// + /// Основной метод перемещения элементов в макете. + /// + /// Что перетаскиваем. + /// Куда приземляем. + /// Позиция относительно цели. + /// + /// Если true, контент будет добавлен как документ в центральную область. + /// + public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false) + { + if (source == target) return; + + // 1. Удаляем источник из текущего местоположения + bool sourceRemoved = false; + + if (Root != null && IsDescendantOf(source, Root)) + { + Root = DockOperations.Remove(source, Root); + sourceRemoved = true; + } + else + { + sourceRemoved = RemoveFromFloatingWindows(source); + } + + if (!sourceRemoved) + { + // Проверяем автоскрываемые панели + var autoHidePanel = _autoHidePanels.FirstOrDefault(p => p.Content == source); + if (autoHidePanel != null) + { + _autoHidePanels.Remove(autoHidePanel); + sourceRemoved = true; + AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); + } + } + + if (!sourceRemoved) return; + + // 2. Вставляем в цель + if (target == null) + { + FloatingWindows.Add(new DockWindow { Root = source as IDockElement }); + } + else + { + if (IsDescendantOf(target, Root)) + { + Root = DockOperations.Insert(target, source, position, Root!); + } + else + { + InsertIntoFloatingWindow(target, source, position); + } + } + + LayoutUpdated?.Invoke(); + } + + private bool RemoveFromFloatingWindows(IDockElement element) + { + foreach (var win in FloatingWindows.ToArray()) + { + if (win.Root != null && IsDescendantOf(element, win.Root)) + { + win.Root = DockOperations.Remove(element, win.Root); + if (win.Root == null) + FloatingWindows.Remove(win); + return true; + } + } + return false; + } + + private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position) + { + foreach (var win in FloatingWindows) + { + if (win.Root != null && IsDescendantOf(target, win.Root)) + { + win.Root = DockOperations.Insert(target, source, position, win.Root); + return; + } + } + } + + private bool IsDescendantOf(IDockElement element, IDockElement ancestor) + { + if (element == ancestor) return true; + if (ancestor is DockGroup group) + return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second); + return false; + } + + /// Поиск элемента по ID во всех окнах. + public IDockElement? FindById(string id) + { + var found = FindRecursive(Root, id); + if (found != null) return found; + + foreach (var win in FloatingWindows) + { + found = FindRecursive(win.Root, id); + if (found != null) return found; + } + return null; + } + + private IDockElement? FindRecursive(IDockElement? node, string id) + { + if (node == null || node.Id == id) return node; + if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id); + return null; + } + + /// + /// Сбрасывает макет к состоянию по умолчанию. + /// + public void Reset() + { + Root = null; + FloatingWindows.Clear(); + _autoHidePanels.Clear(); + LayoutUpdated?.Invoke(); + AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Обрабатывает операцию перетаскивания между элементами. + /// + /// Источник перетаскивания. + /// Цель сброса. + /// Позиция сброса относительно цели. + /// Данные перетаскивания. + /// true, если операция успешно выполнена; иначе false. + public bool HandleDragDrop(IDockElement source, IDockElement target, + DockPosition position, object data) + { + try + { + if (source == target) + return false; + + // Определяем тип операции на основе данных + if (data is ContentDragData contentData) + { + return HandleContentDragDrop(contentData, target, position); + } + else if (data is DockElementDragData elementData) + { + return HandleElementDragDrop(elementData, target, position); + } + + return false; + } + catch (Exception ex) + { + DragDropOperation?.Invoke(this, new DragDropEventArgs( + source, target, position, false, ex.Message)); + return false; + } + } + + private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position) + { + // Находим исходный контейнер с контентом + var sourceContainer = FindElementById(data.ElementId) as IDockContainer; + if (sourceContainer == null) + return false; + + // Находим контент + var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId); + if (content == null) + return false; + + if (target is IDockContainer targetContainer && position == DockPosition.Center) + { + // Объединение вкладок + sourceContainer.RemoveContent(content); + targetContainer.AddContent(content); + + DragDropOperation?.Invoke(this, new DragDropEventArgs( + sourceContainer as IDockElement ?? sourceContainer as IDockElement, + target, position, true, "Content merged")); + return true; + } + + return false; + } + + private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position) + { + // Находим перетаскиваемый элемент + var sourceElement = FindElementById(data.ElementId); + if (sourceElement == null) + return false; + + // Выполняем перемещение + Move(sourceElement, target, position); + + DragDropOperation?.Invoke(this, new DragDropEventArgs( + sourceElement, target, position, true, "Element moved")); + return true; + } + + /// + /// Находит элемент по идентификатору. + /// + public IDockElement? FindElementById(string id) + { + return FindElementByIdRecursive(Root, id) ?? + FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id)) + .FirstOrDefault(result => result != null); + } + + private IDockElement? FindElementByIdRecursive(IDockElement? element, string id) + { + if (element == null) return null; + if (element.Id == id) return element; + + if (element is DockGroup group) + { + return FindElementByIdRecursive(group.First, id) ?? + FindElementByIdRecursive(group.Second, id); + } + + return null; + } +} + +/// +/// Аргументы события операции перетаскивания. +/// +public class DragDropEventArgs : EventArgs +{ + /// Источник перетаскивания. + public IDockElement Source { get; } + + /// Цель сброса. + public IDockElement Target { get; } + + /// Позиция сброса. + public DockPosition Position { get; } + + /// Показывает, была ли операция успешной. + public bool Success { get; } + + /// Сообщение о результате операции. + public string Message { get; } + + /// Время выполнения операции. + public DateTime Timestamp { get; } + + public DragDropEventArgs(IDockElement source, IDockElement target, + DockPosition position, bool success, string message) + { + Source = source; + Target = target; + Position = position; + Success = success; + Message = message; + Timestamp = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Lattice.Core.Docking.csproj b/Lattice.Core.Docking/Lattice.Core.Docking.csproj new file mode 100644 index 0000000..a05bde6 --- /dev/null +++ b/Lattice.Core.Docking/Lattice.Core.Docking.csproj @@ -0,0 +1,13 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + + + + + + + diff --git a/Lattice.Core.Docking/Models/AutoHidePanel.cs b/Lattice.Core.Docking/Models/AutoHidePanel.cs new file mode 100644 index 0000000..03df5ed --- /dev/null +++ b/Lattice.Core.Docking/Models/AutoHidePanel.cs @@ -0,0 +1,114 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Lattice.Core.Docking.Models; + +/// +/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна. +/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора. +/// +/// +/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE, +/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам. +/// +public class AutoHidePanel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? name = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + private bool _isVisible = false; + private double _slideOffset = 0; + + /// + /// Уникальный идентификатор автоскрываемой панели. + /// + public string Id { get; } = Guid.NewGuid().ToString(); + + /// + /// Содержимое панели. + /// + public Abstractions.IDockContent Content { get; set; } + + /// + /// Сторона окна, к которой прикреплена панель. + /// + public DockSide Side { get; set; } + + /// + /// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон). + /// + public double Size { get; set; } = 300; + + /// + /// Признак видимости панели. + /// + public bool IsVisible + { + get => _isVisible; + set + { + if (_isVisible != value) + { + _isVisible = value; + OnPropertyChanged(); + } + } + } + + /// + /// Смещение для анимации выезда/заезда панели (0-1). + /// + public double SlideOffset + { + get => _slideOffset; + set + { + if (Math.Abs(_slideOffset - value) > 0.001) + { + _slideOffset = value; + OnPropertyChanged(); + } + } + } + + /// + /// Заголовок панели (обычно берется из содержимого). + /// + public string Title => Content?.Title ?? "Auto-hide Panel"; + + /// + /// Инициализирует новый экземпляр автоскрываемой панели. + /// + /// Содержимое панели. + /// Сторона окна для прикрепления. + public AutoHidePanel(Abstractions.IDockContent content, DockSide side) + { + Content = content ?? throw new ArgumentNullException(nameof(content)); + Side = side; + } + + /// + /// Переключает видимость панели. + /// + public void Toggle() + { + IsVisible = !IsVisible; + } + + /// + /// Показывает панель. + /// + public void Show() + { + IsVisible = true; + } + + /// + /// Скрывает панель. + /// + public void Hide() + { + IsVisible = false; + } +} diff --git a/Lattice.Core.Docking/Models/DockGroup.cs b/Lattice.Core.Docking/Models/DockGroup.cs new file mode 100644 index 0000000..08bc6ff --- /dev/null +++ b/Lattice.Core.Docking/Models/DockGroup.cs @@ -0,0 +1,444 @@ +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.DragDrop.Abstractions; +using Lattice.Core.DragDrop.Enums; +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Lattice.Core.Docking.Models; + +/// +/// Представляет узел дерева компоновки, который разделяет доступную область +/// между двумя дочерними элементами. Этот класс является основным структурным +/// элементом для создания сложных макетов с разделителями. +/// +/// +/// +/// реализует как (для +/// возможности перетаскивания всей группы), так и +/// (для возможности сброса на группу), что делает его полностью интегрированным +/// в систему перетаскивания док-системы. +/// +/// +/// Каждая группа содержит два дочерних элемента ( и +/// ), которые могут быть либо другими группами (для +/// создания вложенной структуры), либо листами () +/// с контентом. Направление разделения определяется свойством +/// . +/// +/// +public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged +{ + /// + /// Событие, возникающее при изменении значения свойства. + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private double _splitRatio = 0.5; + private string _id; + + /// + /// Получает уникальный идентификатор группы. + /// + /// + /// Строковый идентификатор, уникальный в пределах дерева компоновки. + /// + /// + /// Идентификатор используется для сериализации/десериализации макета, + /// поиска элементов и отслеживания изменений в дереве. + /// + public string Id + { + get => _id; + internal set + { + if (_id != value) + { + _id = value; + OnPropertyChanged(); + } + } + } + + /// + /// Получает или задает родительский элемент в иерархии дерева компоновки. + /// + /// + /// Родительский элемент или null, если эта группа является корневой. + /// + /// + /// Это свойство управляется системой компоновки при добавлении или + /// удалении элементов из дерева. + /// + public IDockElement? Parent { get; set; } + + /// + /// Получает или задает первый дочерний элемент (левую или верхнюю область). + /// + /// + /// Элемент, занимающий первую часть разделенной области. + /// + /// + /// Выбрасывается при попытке установить значение null. + /// + /// + /// При установке нового значения автоматически обновляется свойство + /// у дочернего элемента. + /// + public IDockElement First { get; set; } + + /// + /// Получает или задает второй дочерний элемент (правую или нижнюю область). + /// + /// + /// Элемент, занимающий вторую часть разделенной области. + /// + /// + /// Выбрасывается при попытке установить значение null. + /// + /// + /// При установке нового значения автоматически обновляется свойство + /// у дочернего элемента. + /// + public IDockElement Second { get; set; } + + /// + /// Получает или задает направление разделения данной группы. + /// + /// + /// Значение перечисления , указывающее, + /// как разделена область: горизонтально или вертикально. + /// + /// + /// + /// создает левую и правую области. + /// + /// + /// создает верхнюю и нижнюю области. + /// + /// + public SplitDirection Orientation { get; set; } + + /// + /// Получает или задает соотношение разделения между первым и вторым элементами. + /// + /// + /// Значение от 0.0 до 1.0, где: + /// + /// 0.0 - вся область принадлежит второму элементу + /// 0.5 - область разделена поровну + /// 1.0 - вся область принадлежит первому элементу + /// + /// + /// + /// Изменение этого свойства вызывает событие + /// и может привести к перерисовке пользовательского интерфейса. + /// + public double SplitRatio + { + get => _splitRatio; + set + { + if (Math.Abs(_splitRatio - value) > double.Epsilon) + { + _splitRatio = value; + OnPropertyChanged(); + } + } + } + + /// + /// Получает или задает желаемую ширину элемента. + /// + /// + /// Ширина в пикселях или относительных единицах. + /// + public double Width { get; set; } + + /// + /// Получает или задает желаемую высоту элемента. + /// + /// + /// Высота в пикселях или относительных единицах. + /// + public double Height { get; set; } + + /// + /// Получает минимально допустимую ширину элемента. + /// + /// + /// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность. + /// + /// + /// Для группы минимальная ширина вычисляется как сумма минимальных ширин + /// дочерних элементов при горизонтальной ориентации или максимум минимальных + /// ширин при вертикальной ориентации. + /// + public double MinWidth => Orientation == SplitDirection.Horizontal + ? First.MinWidth + Second.MinWidth + : Math.Max(First.MinWidth, Second.MinWidth); + + /// + /// Получает минимально допустимую высоту элемента. + /// + /// + /// Минимальная высота в пикселях, при которой элемент сохраняет функциональность. + /// + /// + /// Для группы минимальная высота вычисляется как сумма минимальных высот + /// дочерних элементов при вертикальной ориентации или максимум минимальных + /// высот при горизонтальной ориентации. + /// + public double MinHeight => Orientation == SplitDirection.Vertical + ? First.MinHeight + Second.MinHeight + : Math.Max(First.MinHeight, Second.MinHeight); + + /// + /// Инициализирует новый экземпляр класса . + /// + /// + /// Первый дочерний элемент (левая или верхняя область). + /// + /// + /// Второй дочерний элемент (правая или нижняя область). + /// + /// + /// Направление разделения между дочерними элементами. + /// + /// + /// Уникальный идентификатор группы. Если не указан, генерируется новый GUID. + /// + /// + /// Выбрасывается, когда или + /// равны null. + /// + /// + /// Конструктор автоматически устанавливает свойство + /// у дочерних элементов на текущую группу и генерирует уникальный идентификатор, + /// если он не был предоставлен. + /// + public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null) + { + First = first ?? throw new ArgumentNullException(nameof(first)); + Second = second ?? throw new ArgumentNullException(nameof(second)); + Orientation = orientation; + Id = id ?? Guid.NewGuid().ToString(); + + First.Parent = this; + Second.Parent = this; + } + + /// + /// Вызывает событие . + /// + /// + /// Имя изменившегося свойства. Если не указано, определяется автоматически. + /// + protected void OnPropertyChanged([CallerMemberName] string? name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + #region Реализация IDragSource + + /// + /// Определяет, может ли группа начать операцию перетаскивания. + /// + /// + /// При успешном возврате содержит информацию о перетаскивании; + /// в противном случае — null. + /// + /// + /// true, если группа может начать перетаскивание; в противном случае — false. + /// + /// + /// + /// Группа может быть перетащена только если она не является корневым + /// элементом дерева (имеет родителя). + /// + /// + /// При успешной проверке метод заполняет + /// данными типа . + /// + /// + public bool CanStartDrag(out DragInfo? dragInfo) + { + dragInfo = null; + + // DockGroup можно перетаскивать только если он не является корневым элементом + if (Parent == null) + return false; + + // Создаем данные для перетаскивания + var data = new DockElementDragData + { + ElementId = Id, + ElementType = GetType().Name, + IsGroup = true + }; + + dragInfo = new DragInfo(data, DragDropEffects.Move, Point.Zero, this); + return true; + } + + /// + /// Начинает операцию перетаскивания для группы. + /// + /// + /// Информация о перетаскивании, полученная из . + /// + /// + /// Всегда возвращает true, так как группа не требует специальной подготовки + /// для начала перетаскивания. + /// + /// + /// Для этот метод не выполняет дополнительных действий, + /// так как все необходимые данные уже содержатся в . + /// + public bool StartDrag(DragInfo dragInfo) + { + // DockGroup не требует дополнительной подготовки для перетаскивания + return true; + } + + /// + /// Вызывается при завершении операции перетаскивания. + /// + /// + /// Исходная информация о перетаскивании. + /// + /// + /// Эффекты, которые были применены при сбросе. + /// + /// + /// + /// Этот метод вызывается после того, как операция перетаскивания была + /// завершена (успешно или неуспешно). + /// + /// + /// Для этот метод не выполняет действий, так как + /// все изменения в структуре дерева уже обработаны . + /// + /// + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + // Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение + } + + /// + /// Вызывается при отмене операции перетаскивания. + /// + /// + /// Исходная информация о перетаскивании. + /// + /// + /// Для отмена перетаскивания не требует специальных + /// действий, так как структура дерева не была изменена. + /// + public void DragCancelled(DragInfo dragInfo) + { + // Отмена перетаскивания не требует действий + } + + #endregion + + #region Реализация IDropTarget + + /// + /// Определяет, может ли группа принять сбрасываемые данные. + /// + /// + /// Информация о потенциальном сбросе. + /// + /// + /// true, если группа может принять данные; в противном случае — false. + /// + /// + /// + /// Группа может принимать только данные типа + /// для элементов док-системы ( или ). + /// + /// + /// Группа не может принять сброс самой себя (проверяется по идентификатору). + /// + /// + public bool CanAcceptDrop(DropInfo dropInfo) + { + if (dropInfo.Data is not DockElementDragData dragData) + return false; + + // Нельзя сбросить элемент на самого себя + if (dragData.ElementId == Id) + return false; + + // Можно принимать только элементы док-системы + return dragData.ElementType == nameof(DockGroup) || dragData.ElementType == nameof(DockLeaf); + } + + /// + /// Вызывается, когда перетаскиваемый объект находится над группой. + /// + /// + /// Информация о текущем положении перетаскивания. + /// + /// + /// + /// Этот метод вызывается постоянно, пока пользователь перемещает объект + /// над целью. Для группы он устанавливает предлагаемые эффекты в + /// . + /// + /// + /// Если группа может принять сброс, предлагается эффект перемещения; + /// в противном случае эффекты не предлагаются. + /// + /// + public void DragOver(DropInfo dropInfo) + { + if (CanAcceptDrop(dropInfo)) + { + dropInfo.SuggestedEffects = DragDropEffects.Move; + } + else + { + dropInfo.SuggestedEffects = DragDropEffects.None; + } + } + + /// + /// Вызывается, когда пользователь сбрасывает данные на группу. + /// + /// + /// Информация о сбросе. + /// + /// + /// + /// Для обработка сброса делегируется + /// , поэтому метод просто помечает операцию + /// как обработанную. + /// + /// + /// Фактическое изменение структуры дерева выполняется менеджером макета + /// на основе данных из . + /// + /// + public void Drop(DropInfo dropInfo) + { + // Обработка сброса делегируется LayoutManager + dropInfo.MarkAsHandled(); + } + + /// + /// Вызывается, когда перетаскиваемый объект покидает область группы. + /// + /// + /// Для группы этот метод не выполняет действий, так как очистка визуальной + /// обратной связи выполняется в UI-слое. + /// + public void DragLeave() + { + // Очистка визуальной обратной связи (будет выполнена в UI слое) + } + + #endregion +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Models/DockLeaf.cs b/Lattice.Core.Docking/Models/DockLeaf.cs new file mode 100644 index 0000000..f8a7537 --- /dev/null +++ b/Lattice.Core.Docking/Models/DockLeaf.cs @@ -0,0 +1,580 @@ +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.DragDrop.Abstractions; +using Lattice.Core.DragDrop.Enums; +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Lattice.Core.Docking.Models; + +/// +/// Представляет конечный узел (лист) дерева компоновки, который непосредственно +/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для +/// отображаемого пользователю содержимого. +/// +/// +/// +/// реализует интерфейсы , +/// и , что позволяет ему: +/// +/// +/// Управлять коллекцией вкладок +/// Быть источником перетаскивания (как всего листа, так и отдельных вкладок) +/// Принимать сброс других элементов или вкладок +/// +/// +/// Лист является основным элементом, с которым взаимодействует пользователь +/// при работе с документами или инструментальными панелями в IDE-подобных +/// приложениях. +/// +/// +public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget +{ + /// + /// Событие, возникающее при изменении значения свойства. + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private readonly ObservableCollection _items = new(); + private IDockContent? _activeContent; + private string _id; + + /// + /// Получает уникальный идентификатор листа. + /// + /// + /// Строковый идентификатор, уникальный в пределах дерева компоновки. + /// + public string Id + { + get => _id; + internal set + { + if (_id != value) + { + _id = value; + OnPropertyChanged(); + } + } + } + + /// + /// Получает или задает родительский элемент в иерархии дерева компоновки. + /// + /// + /// Родительский элемент или null, если этот лист является корневым. + /// + public IDockElement? Parent { get; set; } + + /// + /// Получает список вкладок, содержащихся в данном контейнере. + /// + /// + /// Коллекция объектов, реализующих . + /// + /// + /// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет + /// автоматически обновлять пользовательский интерфейс при добавлении или + /// удалении вкладок. + /// + public IList Children => _items; + + /// + /// Получает или задает активную (выбранную) вкладку в контейнере. + /// + /// + /// Активная вкладка или null, если в контейнере нет вкладок. + /// + /// + /// + /// При установке нового значения проверяется, что вкладка действительно + /// содержится в коллекции . + /// + /// + /// Изменение этого свойства вызывает событие . + /// + /// + public IDockContent? ActiveContent + { + get => _activeContent; + set + { + if (value != null && !_items.Contains(value)) return; + if (_activeContent != value) + { + _activeContent = value; + OnPropertyChanged(); + } + } + } + + /// + /// Получает или задает желаемую ширину элемента. + /// + /// + /// Ширина в пикселях или относительных единицах. + /// + public double Width { get; set; } + + /// + /// Получает или задает желаемую высоту элемента. + /// + /// + /// Высота в пикселях или относительных единицах. + /// + public double Height { get; set; } + + /// + /// Получает или задает минимально допустимую ширину элемента. + /// + /// + /// Минимальная ширина в пикселях. Значение по умолчанию: 100. + /// + public double MinWidth { get; set; } = 100; + + /// + /// Получает или задает минимально допустимую высоту элемента. + /// + /// + /// Минимальная высота в пикселях. Значение по умолчанию: 100. + /// + public double MinHeight { get; set; } = 100; + + /// + /// Получает или задает положение полосы вкладок в контейнере. + /// + /// + /// Значение перечисления , определяющее, + /// где располагаются вкладки относительно содержимого. + /// + /// + /// Поддерживаются все четыре стороны: верх, низ, лево, право. + /// + public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom; + + /// + /// Инициализирует новый экземпляр класса . + /// + /// + /// Уникальный идентификатор листа. Если не указан, генерируется новый GUID. + /// + /// + /// Создает пустой лист с коллекцией вкладок и генерирует уникальный + /// идентификатор, если он не был предоставлен. + /// + public DockLeaf(string? id = null) + { + _id = id ?? Guid.NewGuid().ToString(); + } + + /// + /// Вызывает событие . + /// + /// + /// Имя изменившегося свойства. Если не указано, определяется автоматически. + /// + protected void OnPropertyChanged([CallerMemberName] string? name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + /// + /// Добавляет контент в контейнер и делает его активным. + /// + /// + /// Контент для добавления. + /// + /// + /// + /// Если контент уже содержится в коллекции, он не добавляется повторно, + /// но становится активным. + /// + /// + /// Этот метод обновляет свойство и вызывает + /// соответствующее событие изменения свойства. + /// + /// + public void AddContent(IDockContent content) + { + if (!_items.Contains(content)) + { + _items.Add(content); + } + ActiveContent = content; + } + + /// + /// Удаляет контент из контейнера. + /// + /// + /// Контент для удаления. + /// + /// + /// + /// Если удаляемый контент является активным, автоматически выбирается + /// новая активная вкладка (следующая в списке или предыдущая, если удалена + /// последняя). + /// + /// + /// Если после удаления контейнер становится пустым, он может быть удален + /// из дерева макета системой компоновки. + /// + /// + public void RemoveContent(IDockContent content) + { + int index = _items.IndexOf(content); + if (index == -1) return; + + _items.RemoveAt(index); + + if (ActiveContent == content) + { + if (_items.Count > 0) + ActiveContent = _items[Math.Min(index, _items.Count - 1)]; + else + ActiveContent = null; + } + } + + #region Реализация IDragSource + + /// + /// Определяет, может ли лист начать операцию перетаскивания. + /// + /// + /// При успешном возврате содержит информацию о перетаскивании; + /// в противном случае — null. + /// + /// + /// true, если лист может начать перетаскивание; в противном случае — false. + /// + /// + /// + /// Лист может быть перетащен, если: + /// + /// + /// Он имеет родителя (не является корневым) + /// Или имеет хотя бы одну вкладку (не пустой) + /// + /// + /// В зависимости от наличия активного контента создаются разные данные: + /// + /// + /// + /// Если есть активный контент - создается + /// для перетаскивания конкретной вкладки + /// + /// + /// Если нет активного контента - создается + /// для перетаскивания всего листа + /// + /// + /// + public bool CanStartDrag(out DragInfo? dragInfo) + { + dragInfo = null; + + // DockLeaf можно перетаскивать + if (Parent == null && Children.Count == 0) + return false; // Не перетаскиваем пустые корневые листья + + object data; + + // Если есть активный контент, перетаскиваем контент, иначе перетаскиваем весь лист + if (ActiveContent != null) + { + data = new ContentDragData + { + ElementId = Id, + ContentId = ActiveContent.Id, + ContentTitle = ActiveContent.Title, + ContentType = ActiveContent.GetType().Name + }; + } + else + { + data = new DockElementDragData + { + ElementId = Id, + ElementType = GetType().Name, + IsGroup = false, + Width = Width, + Height = Height + }; + } + + dragInfo = new DragInfo(data, DragDropEffects.Move | DragDropEffects.Copy, Point.Zero, this); + return true; + } + + /// + /// Начинает операцию перетаскивания для листа. + /// + /// + /// Информация о перетаскивании. + /// + /// + /// Всегда возвращает true. + /// + /// + /// Для этот метод не выполняет дополнительных действий. + /// + public bool StartDrag(DragInfo dragInfo) + { + // DockLeaf не требует дополнительной подготовки + return true; + } + + /// + /// Вызывается при завершении операции перетаскивания. + /// + /// + /// Исходная информация о перетаскивании. + /// + /// + /// Эффекты, которые были применены при сбросе. + /// + /// + /// Для этот метод не выполняет действий. + /// + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + // Если лист был перемещен или скопирован, LayoutManager уже обработал это + } + + /// + /// Вызывается при отмене операции перетаскивания. + /// + /// + /// Исходная информация о перетаскивании. + /// + /// + /// Для отмена перетаскивания не требует действий. + /// + public void DragCancelled(DragInfo dragInfo) + { + // Отмена не требует действий + } + + #endregion + + #region Реализация IDropTarget + + /// + /// Определяет, может ли лист принять сбрасываемые данные. + /// + /// + /// Информация о потенциальном сбросе. + /// + /// + /// true, если лист может принять данные; в противном случае — false. + /// + /// + /// Лист может принимать: + /// + /// + /// для других листов и групп + /// (для объединения или разделения) + /// + /// + /// для вкладок (для объединения вкладок) + /// + /// + /// + public bool CanAcceptDrop(DropInfo dropInfo) + { + if (dropInfo.Data is DockElementDragData elementData) + { + // Можно принимать другие листы и группы + return elementData.ElementType == nameof(DockLeaf) || + elementData.ElementType == nameof(DockGroup); + } + else if (dropInfo.Data is ContentDragData contentData) + { + // Можно принимать контент для объединения вкладок + return true; + } + + return false; + } + + /// + /// Вызывается, когда перетаскиваемый объект находится над листом. + /// + /// + /// Информация о текущем положении перетаскивания. + /// + /// + /// + /// В зависимости от типа данных устанавливаются разные предлагаемые эффекты: + /// + /// + /// + /// Для - эффект копирования (объединение вкладок) + /// + /// + /// Для - эффект перемещения + /// + /// + /// + public void DragOver(DropInfo dropInfo) + { + if (CanAcceptDrop(dropInfo)) + { + if (dropInfo.Data is ContentDragData) + { + // Для контента предлагаем копирование (объединение вкладок) + dropInfo.SuggestedEffects = DragDropEffects.Copy; + } + else + { + // Для элементов предлагаем перемещение + dropInfo.SuggestedEffects = DragDropEffects.Move; + } + } + else + { + dropInfo.SuggestedEffects = DragDropEffects.None; + } + } + + /// + /// Вызывается, когда пользователь сбрасывает данные на лист. + /// + /// + /// Информация о сбросе. + /// + /// + /// Обработка сброса делегируется . + /// + public void Drop(DropInfo dropInfo) + { + // Обработка делегируется LayoutManager + dropInfo.MarkAsHandled(); + } + + /// + /// Вызывается, когда перетаскиваемый объект покидает область листа. + /// + /// + /// Очистка визуальной обратной связи выполняется в UI-слое. + /// + public void DragLeave() + { + // Очистка визуальной обратной связи + } + + #endregion +} + +/// +/// Представляет данные для перетаскивания элементов док-системы (групп или листов). +/// Используется при перетаскивании целых структурных элементов дерева компоновки. +/// +/// +/// Этот класс сериализуется и передается между компонентами системы перетаскивания +/// для идентификации перетаскиваемого элемента и его свойств. +/// +public class DockElementDragData +{ + /// + /// Получает или задает уникальный идентификатор элемента. + /// + /// + /// Идентификатор элемента, соответствующий свойству . + /// + public string ElementId { get; set; } = string.Empty; + + /// + /// Получает или задает тип элемента. + /// + /// + /// Имя типа элемента (обычно "DockGroup" или "DockLeaf"). + /// + public string ElementType { get; set; } = string.Empty; + + /// + /// Получает или задает значение, указывающее, является ли элемент группой. + /// + /// + /// true, если элемент является ; false, если . + /// + public bool IsGroup { get; set; } + + /// + /// Получает или задает идентификатор родительского элемента. + /// + /// + /// Идентификатор родительского элемента или null, если элемент корневой. + /// + public string? ParentId { get; set; } + + /// + /// Получает или задает ширину элемента. + /// + /// + /// Текущая ширина элемента в пикселях. + /// + public double Width { get; set; } + + /// + /// Получает или задает высоту элемента. + /// + /// + /// Текущая высота элемента в пикселях. + /// + public double Height { get; set; } +} + +/// +/// Представляет данные для перетаскивания контента (вкладок). +/// Используется при перетаскивании отдельных вкладок между контейнерами. +/// +/// +/// Этот класс позволяет идентифицировать конкретную вкладку для операций +/// объединения или перемещения между контейнерами. +/// +public class ContentDragData +{ + /// + /// Получает или задает идентификатор контейнера (листа), содержащего контент. + /// + /// + /// Идентификатор , в котором находится перетаскиваемая вкладка. + /// + public string ElementId { get; set; } = string.Empty; + + /// + /// Получает или задает уникальный идентификатор контента. + /// + /// + /// Идентификатор контента, соответствующий свойству . + /// + public string ContentId { get; set; } = string.Empty; + + /// + /// Получает или задает заголовок контента. + /// + /// + /// Текст, отображаемый на вкладке. + /// + public string ContentTitle { get; set; } = string.Empty; + + /// + /// Получает или задает тип контента. + /// + /// + /// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.). + /// + public string ContentType { get; set; } = string.Empty; + + /// + /// Получает или задает значение, указывающее, можно ли закрыть контент. + /// + /// + /// true, если контент можно закрыть; в противном случае — false. + /// + public bool CanClose { get; set; } = true; +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Models/DockPosition.cs b/Lattice.Core.Docking/Models/DockPosition.cs new file mode 100644 index 0000000..f34ea72 --- /dev/null +++ b/Lattice.Core.Docking/Models/DockPosition.cs @@ -0,0 +1,13 @@ +namespace Lattice.Core.Docking.Models; + +/// +/// Определяет позицию вставки при операции Drag-and-Drop. +/// +public enum DockPosition +{ + Left, + Right, + Top, + Bottom, + Center, +} diff --git a/Lattice.Core.Docking/Models/DockSide.cs b/Lattice.Core.Docking/Models/DockSide.cs new file mode 100644 index 0000000..6975ee6 --- /dev/null +++ b/Lattice.Core.Docking/Models/DockSide.cs @@ -0,0 +1,19 @@ +namespace Lattice.Core.Docking.Models; + +/// +/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели. +/// +public enum DockSide +{ + /// Левая сторона окна. + Left, + + /// Правая сторона окна. + Right, + + /// Верхняя сторона окна. + Top, + + /// Нижняя сторона окна. + Bottom +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Models/DockWindow.cs b/Lattice.Core.Docking/Models/DockWindow.cs new file mode 100644 index 0000000..cb73925 --- /dev/null +++ b/Lattice.Core.Docking/Models/DockWindow.cs @@ -0,0 +1,23 @@ +using Lattice.Core.Docking.Abstractions; + +namespace Lattice.Core.Docking.Models; + +/// +/// Описывает состояние плавающего окна в системе Lattice. +/// +public class DockWindow +{ + /// Уникальный ID окна для сохранения его позиции в конфиге. + public string Id { get; } = Guid.NewGuid().ToString(); + + /// Корневой элемент макета внутри данного окна. + public IDockElement? Root { get; set; } + + public double X { get; set; } + public double Y { get; set; } + public double Width { get; set; } = 800; + public double Height { get; set; } = 600; + + /// Заголовок окна (обычно берется из активного контента). + public string Title { get; set; } = "Lattice Tool Window"; +} diff --git a/Lattice.Core.Docking/Models/SplitDirection.cs b/Lattice.Core.Docking/Models/SplitDirection.cs new file mode 100644 index 0000000..faee1bf --- /dev/null +++ b/Lattice.Core.Docking/Models/SplitDirection.cs @@ -0,0 +1,12 @@ +namespace Lattice.Core.Docking.Models; + +/// +/// Перечисление направлений разделения пространства внутри группы. +/// +public enum SplitDirection +{ + /// Разделение по горизонтали (создает левую и правую области). + Horizontal, + /// Разделение по вертикали (создает верхнюю и нижнюю области). + Vertical +} diff --git a/Lattice.Core.Docking/Models/TabPlacement.cs b/Lattice.Core.Docking/Models/TabPlacement.cs new file mode 100644 index 0000000..5dbd7d4 --- /dev/null +++ b/Lattice.Core.Docking/Models/TabPlacement.cs @@ -0,0 +1,12 @@ +namespace Lattice.Core.Docking.Models; + +/// +/// Определяет положение полосы вкладок в контейнере. +/// +public enum TabPlacement +{ + Top, + Bottom, + Left, + Right, +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs b/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs new file mode 100644 index 0000000..a4b6246 --- /dev/null +++ b/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs @@ -0,0 +1,31 @@ +namespace Lattice.Core.Docking.Serialization; + +/// +/// Абстракция для сериализации и десериализации состояния макета док-системы. +/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние. +/// +/// +/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный) +/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы. +/// +public interface ILayoutSerializer +{ + /// + /// Сериализует состояние менеджера макета в строку. + /// + /// Менеджер макета для сериализации. + /// Строковое представление состояния макета. + string Serialize(Engine.LayoutManager manager); + + /// + /// Десериализует состояние макета из строки и восстанавливает его в менеджере. + /// + /// Менеджер макета для восстановления состояния. + /// Сериализованное состояние макета. + /// + /// Функция разрешения контента по идентификатору, используемая для восстановления + /// ссылок на контент в десериализованном состоянии. + /// + void Deserialize(Engine.LayoutManager manager, string serializedLayout, + Func contentResolver); +} diff --git a/Lattice.Core.Docking/Serialization/ISerializableLayout.cs b/Lattice.Core.Docking/Serialization/ISerializableLayout.cs new file mode 100644 index 0000000..92167ed --- /dev/null +++ b/Lattice.Core.Docking/Serialization/ISerializableLayout.cs @@ -0,0 +1,19 @@ +namespace Lattice.Core.Docking.Serialization; + +/// +/// Контракт для объектов, которые могут предоставлять состояние для сериализации. +/// +public interface ISerializableLayout +{ + /// + /// Получает состояние для сериализации. + /// + /// Объект состояния, готовый к сериализации. + object GetSerializableState(); + + /// + /// Восстанавливает состояние из десериализованного объекта. + /// + /// Десериализованное состояние. + void RestoreFromState(object state); +} \ No newline at end of file diff --git a/Lattice.Core.Docking/Services/ContentRegistry.cs b/Lattice.Core.Docking/Services/ContentRegistry.cs new file mode 100644 index 0000000..0daee2c --- /dev/null +++ b/Lattice.Core.Docking/Services/ContentRegistry.cs @@ -0,0 +1,158 @@ +namespace Lattice.Core.Docking.Services; + +/// +/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу. +/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE. +/// +/// +/// Реализует шаблон "Фабрика" для создания экземпляров . +/// Позволяет регистрировать фабричные методы для различных типов контента, что обеспечивает +/// позднее связывание и возможность плагинной архитектуры. +/// +public class ContentRegistry +{ + private readonly Dictionary _contentTypes = new(); + + /// + /// Регистрирует фабричный метод для создания контента указанного типа. + /// + /// Тип контента, реализующий . + /// Уникальный идентификатор типа контента. + /// Фабричный метод для создания экземпляров контента. + /// Метаданные типа контента (опционально). + /// Выбрасывается, если contentTypeId или factory равны null. + /// Выбрасывается, если contentTypeId уже зарегистрирован. + public void Register(string contentTypeId, Func factory, ContentMetadata? metadata = null) + where T : Abstractions.IDockContent + { + if (string.IsNullOrWhiteSpace(contentTypeId)) + throw new ArgumentNullException(nameof(contentTypeId)); + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + if (_contentTypes.ContainsKey(contentTypeId)) + throw new ArgumentException($"Content type '{contentTypeId}' is already registered."); + + _contentTypes[contentTypeId] = new ContentDescriptor( + typeof(T), + () => factory(), + metadata ?? new ContentMetadata(contentTypeId, typeof(T).Name) + ); + } + + /// + /// Создает новый экземпляр контента указанного типа с заданным идентификатором. + /// + /// Идентификатор типа контента. + /// Уникальный идентификатор для создаваемого экземпляра контента. + /// Новый экземпляр контента. + /// Выбрасывается, если тип контента не зарегистрирован. + public Abstractions.IDockContent CreateContent(string contentTypeId, string id) + { + if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor)) + throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered."); + + var content = descriptor.Factory(); + // Устанавливаем ID через рефлексию, если есть свойство Id + var property = content.GetType().GetProperty("Id"); + if (property != null && property.CanWrite) + { + property.SetValue(content, id); + } + + return content; + } + + /// + /// Получает метаданные для указанного типа контента. + /// + /// Идентификатор типа контента. + /// Метаданные типа контента или null, если тип не найден. + public ContentMetadata? GetMetadata(string contentTypeId) + { + return _contentTypes.TryGetValue(contentTypeId, out var descriptor) + ? descriptor.Metadata + : null; + } + + /// + /// Получает все зарегистрированные типы контента. + /// + /// Коллекция идентификаторов зарегистрированных типов контента. + public IEnumerable GetRegisteredTypes() => _contentTypes.Keys; + + /// + /// Проверяет, зарегистрирован ли указанный тип контента. + /// + public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId); + + /// + /// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных. + /// + private class ContentDescriptor + { + public Type ContentType { get; } + public Func Factory { get; } + public ContentMetadata Metadata { get; } + + public ContentDescriptor(Type contentType, Func factory, ContentMetadata metadata) + { + ContentType = contentType; + Factory = factory; + Metadata = metadata; + } + } +} + +/// +/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI. +/// +public class ContentMetadata +{ + /// + /// Идентификатор типа контента. + /// + public string ContentTypeId { get; } + + /// + /// Отображаемое имя типа контента. + /// + public string DisplayName { get; set; } + + /// + /// Описание типа контента. + /// + public string Description { get; set; } + + /// + /// Имя ресурса для иконки (опционально). + /// + public string? IconResource { get; set; } + + /// + /// Признак того, что контент является документом (а не инструментальной панелью). + /// + public bool IsDocument { get; set; } + + /// + /// Минимальная ширина контента в пикселях. + /// + public double DefaultWidth { get; set; } = 300; + + /// + /// Минимальная высота контента в пикселях. + /// + public double DefaultHeight { get; set; } = 200; + + /// + /// Инициализирует новый экземпляр метаданных контента. + /// + /// Идентификатор типа контента. + /// Отображаемое имя. + public ContentMetadata(string contentTypeId, string displayName) + { + ContentTypeId = contentTypeId; + DisplayName = displayName; + Description = string.Empty; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs b/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs new file mode 100644 index 0000000..4b02109 --- /dev/null +++ b/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs @@ -0,0 +1,115 @@ +using Lattice.Core.DragDrop.Abstractions; +using Lattice.Core.DragDrop.Enums; +using Lattice.Core.DragDrop.Models; +using Lattice.Core.DragDrop.Services; +using Lattice.Core.Geometry; +using Moq; +using Xunit; + +namespace Lattice.Core.DragDrop.Tests; + +public class DragDropServiceTests +{ + [Fact] + public void StartDrag_WithValidSource_StartsDragOperation() + { + // Arrange + var service = new DragDropService(); + var mockSource = new Mock(); + var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0)); + + mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true); + mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true); + + // Act + var result = service.StartDrag(mockSource.Object, new Point(0, 0)); + + // Assert + Assert.True(result); + Assert.True(service.IsDragActive); + Assert.NotNull(service.CurrentDragInfo); + } + + [Fact] + public void RegisterDropTarget_ReturnsValidId() + { + // Arrange + var service = new DragDropService(); + var mockTarget = new Mock(); + var bounds = new Rect(0, 0, 100, 100); + + // Act + var id = service.RegisterDropTarget(mockTarget.Object, bounds); + + // Assert + Assert.NotNull(id); + Assert.NotEmpty(id); + } + + [Fact] + public void UpdateDrag_WithValidDropTarget_CallsDragOver() + { + // Arrange + var service = new DragDropService(); + var mockSource = new Mock(); + var mockTarget = new Mock(); + + var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0)); + mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true); + mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true); + + var targetId = service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100)); + service.StartDrag(mockSource.Object, new Point(0, 0)); + + // Act + service.UpdateDrag(new Point(50, 50)); + + // Assert + mockTarget.Verify(t => t.DragOver(It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public void EndDrag_WithValidDrop_CallsDrop() + { + // Arrange + var service = new DragDropService(); + var mockSource = new Mock(); + var mockTarget = new Mock(); + + var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0)); + mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true); + mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true); + + service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100)); + service.StartDrag(mockSource.Object, new Point(0, 0)); + service.UpdateDrag(new Point(50, 50)); + + // Act + var effects = service.EndDrag(new Point(50, 50)); + + // Assert + mockTarget.Verify(t => t.Drop(It.IsAny()), Times.Once()); + Assert.False(service.IsDragActive); + } + + [Fact] + public void CancelDrag_WithActiveDrag_CallsDragCancelled() + { + // Arrange + var service = new DragDropService(); + var mockSource = new Mock(); + var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0)); + + mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true); + mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true); + + service.StartDrag(mockSource.Object, new Point(0, 0)); + + // Act + service.CancelDrag(); + + // Assert + mockSource.Verify(s => s.DragCancelled(It.IsAny()), Times.Once()); + Assert.False(service.IsDragActive); + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj b/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj new file mode 100644 index 0000000..32a369c --- /dev/null +++ b/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs b/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs new file mode 100644 index 0000000..834b788 --- /dev/null +++ b/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs @@ -0,0 +1,28 @@ +namespace Lattice.Core.DragDrop.Abstractions; + +/// +/// Определяет контракт для объектов, которые могут быть источником данных +/// в операции перетаскивания с поддержкой асинхронных операций. +/// +public interface IAsyncDragSource : IDragSource +{ + /// + /// Определяет, может ли объект начать операцию перетаскивания (асинхронно). + /// + Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync(); + + /// + /// Начинает операцию перетаскивания (асинхронно). + /// + Task StartDragAsync(Models.DragInfo dragInfo); + + /// + /// Вызывается при завершении операции перетаскивания (асинхронно). + /// + Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects); + + /// + /// Вызывается при отмене операции перетаскивания (асинхронно). + /// + Task DragCancelledAsync(Models.DragInfo dragInfo); +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs b/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs new file mode 100644 index 0000000..99aa78c --- /dev/null +++ b/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs @@ -0,0 +1,28 @@ +namespace Lattice.Core.DragDrop.Abstractions; + +/// +/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные +/// в операции перетаскивания с поддержкой асинхронных операций. +/// +public interface IAsyncDropTarget : IDropTarget +{ + /// + /// Определяет, может ли объект принять сбрасываемые данные (асинхронно). + /// + Task CanAcceptDropAsync(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда перетаскиваемый объект находится над целью (асинхронно). + /// + Task DragOverAsync(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда пользователь сбрасывает данные на цель (асинхронно). + /// + Task DropAsync(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда перетаскиваемый объект покидает область цели (асинхронно). + /// + Task DragLeaveAsync(); +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Abstractions/IDragSource.cs b/Lattice.Core.DragDrop/Abstractions/IDragSource.cs new file mode 100644 index 0000000..b5fea3b --- /dev/null +++ b/Lattice.Core.DragDrop/Abstractions/IDragSource.cs @@ -0,0 +1,64 @@ +namespace Lattice.Core.DragDrop.Abstractions; + +/// +/// Определяет контракт для объектов, которые могут быть источником данных +/// в операции перетаскивания. +/// +/// +/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания +/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop. +/// +public interface IDragSource +{ + /// + /// Определяет, может ли объект начать операцию перетаскивания. + /// + /// + /// Информация о перетаскивании, которая будет заполнена данными, если операция разрешена. + /// + /// + /// true, если объект может начать перетаскивание; в противном случае — false. + /// + /// + /// Этот метод вызывается системой перетаскивания для проверки возможности + /// начала операции. Если метод возвращает true, он должен заполнить + /// необходимыми данными. + /// + bool CanStartDrag(out Models.DragInfo? dragInfo); + + /// + /// Начинает операцию перетаскивания. + /// + /// Информация о перетаскивании. + /// + /// true, если операция перетаскивания успешно начата; в противном случае — false. + /// + /// + /// Этот метод вызывается, когда пользователь начинает перетаскивание элемента. + /// Реализация должна подготовить данные для перетаскивания и, возможно, + /// создать визуальное представление перетаскиваемого объекта. + /// + bool StartDrag(Models.DragInfo dragInfo); + + /// + /// Вызывается при завершении операции перетаскивания. + /// + /// Исходная информация о перетаскивании. + /// Эффекты, которые были применены при сбросе. + /// + /// Этот метод вызывается после завершения операции перетаскивания + /// (успешного или неуспешного). Реализация может выполнить очистку + /// или обновить состояние на основе результата операции. + /// + void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects); + + /// + /// Вызывается при отмене операции перетаскивания. + /// + /// Исходная информация о перетаскивании. + /// + /// Этот метод вызывается, когда операция перетаскивания была отменена + /// пользователем (например, нажатием клавиши Escape). + /// + void DragCancelled(Models.DragInfo dragInfo); +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs b/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs new file mode 100644 index 0000000..736666e --- /dev/null +++ b/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs @@ -0,0 +1,55 @@ +namespace Lattice.Core.DragDrop.Abstractions; + +/// +/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные +/// в операции перетаскивания. +/// +/// +/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные +/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания. +/// +public interface IDropTarget +{ + /// + /// Определяет, может ли объект принять сбрасываемые данные. + /// + /// Информация о потенциальном сбросе. + /// + /// true, если объект может принять данные; в противном случае — false. + /// + /// + /// Этот метод вызывается, когда перетаскиваемый объект находится над целью. + /// Реализация должна проверить, совместимы ли данные с целью, и установить + /// предлагаемые эффекты в . + /// + bool CanAcceptDrop(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда перетаскиваемый объект находится над целью. + /// + /// Информация о текущем положении перетаскивания. + /// + /// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью. + /// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты. + /// + void DragOver(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда пользователь сбрасывает данные на цель. + /// + /// Информация о сбросе. + /// + /// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью. + /// Реализация должна обработать принятие данных и выполнить соответствующее действие. + /// + void Drop(Models.DropInfo dropInfo); + + /// + /// Вызывается, когда перетаскиваемый объект покидает область цели. + /// + /// + /// Этот метод вызывается, когда пользователь перемещает объект за пределы цели. + /// Реализация должна очистить любую визуальную обратную связь, установленную ранее. + /// + void DragLeave(); +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Enums/DragDropEffects.cs b/Lattice.Core.DragDrop/Enums/DragDropEffects.cs new file mode 100644 index 0000000..808b92f --- /dev/null +++ b/Lattice.Core.DragDrop/Enums/DragDropEffects.cs @@ -0,0 +1,102 @@ +namespace Lattice.Core.DragDrop.Enums; + +/// +/// Определяет эффекты, которые могут быть применены при операции перетаскивания. +/// +/// +/// Этот перечисление используется для указания допустимых операций перетаскивания +/// и передачи информации о результате операции между источником и целью. +/// +[Flags] +public enum DragDropEffects +{ + /// + /// Операция перетаскивания не разрешена. + /// + None = 0, + + /// + /// Данные копируются из источника в цель. + /// + Copy = 1 << 0, + + /// + /// Данные перемещаются из источника в цель. + /// + Move = 1 << 1, + + /// + /// Создается ссылка на исходные данные. + /// + Link = 1 << 2, + + /// + /// Целевой элемент может прокручиваться во время перетаскивания. + /// + Scroll = 1 << 3, + + /// + /// Комбинированный эффект копирования и перемещения. + /// + CopyOrMove = Copy | Move, + + /// + /// Все эффекты разрешены. + /// + All = Copy | Move | Link | Scroll +} + +/// +/// Расширения для работы с DragDropEffects. +/// +public static class DragDropEffectsExtensions +{ + /// + /// Проверяет, содержит ли эффекты указанный эффект. + /// + public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect) + { + return (effects & effect) == effect; + } + + /// + /// Проверяет, содержат ли эффекты копирование. + /// + public static bool CanCopy(this DragDropEffects effects) + { + return effects.HasEffect(DragDropEffects.Copy); + } + + /// + /// Проверяет, содержат ли эффекты перемещение. + /// + public static bool CanMove(this DragDropEffects effects) + { + return effects.HasEffect(DragDropEffects.Move); + } + + /// + /// Проверяет, содержат ли эффекты ссылку. + /// + public static bool CanLink(this DragDropEffects effects) + { + return effects.HasEffect(DragDropEffects.Link); + } + + /// + /// Получает наиболее подходящий эффект на основе модификаторов клавиатуры. + /// + public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey) + { + if (controlKey && altKey) + return DragDropEffects.Link; + if (controlKey) + return DragDropEffects.Copy; + if (shiftKey) + return DragDropEffects.Move; + if (altKey) + return DragDropEffects.Link; + + return DragDropEffects.Move; // По умолчанию + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Enums/DropPosition.cs b/Lattice.Core.DragDrop/Enums/DropPosition.cs new file mode 100644 index 0000000..50e15da --- /dev/null +++ b/Lattice.Core.DragDrop/Enums/DropPosition.cs @@ -0,0 +1,14 @@ +namespace Lattice.Core.DragDrop.Enums; + +/// +/// Позиция сброса относительно цели. +/// +public enum DropPosition +{ + Inside, + Top, + Bottom, + Left, + Right, + Center +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Exceptions/DragDropException.cs b/Lattice.Core.DragDrop/Exceptions/DragDropException.cs new file mode 100644 index 0000000..59874c0 --- /dev/null +++ b/Lattice.Core.DragDrop/Exceptions/DragDropException.cs @@ -0,0 +1,85 @@ +namespace Lattice.Core.DragDrop.Exceptions; + +/// +/// Исключение, возникающее при ошибках в системе перетаскивания. +/// +public class DragDropException : Exception +{ + /// + /// Код ошибки. + /// + public string ErrorCode { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragDropException() + : base("Drag & Drop operation failed.") + { + ErrorCode = "DRAGDROP_0001"; + } + + /// + /// Инициализирует новый экземпляр класса с указанным сообщением. + /// + public DragDropException(string message) + : base(message) + { + ErrorCode = "DRAGDROP_0002"; + } + + /// + /// Инициализирует новый экземпляр класса с кодом ошибки. + /// + public DragDropException(string errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + } + + /// + /// Инициализирует новый экземпляр класса + /// с указанным сообщением и внутренним исключением. + /// + public DragDropException(string message, Exception innerException) + : base(message, innerException) + { + ErrorCode = "DRAGDROP_0003"; + } + + /// + /// Инициализирует новый экземпляр класса + /// с кодом ошибки, сообщением и внутренним исключением. + /// + public DragDropException(string errorCode, string message, Exception innerException) + : base(message, innerException) + { + ErrorCode = errorCode; + } +} + +/// +/// Коды ошибок Drag & Drop системы. +/// +public static class DragDropErrorCodes +{ + // Общие ошибки + public const string OperationAlreadyActive = "DRAGDROP_1001"; + public const string OperationNotActive = "DRAGDROP_1002"; + public const string InvalidData = "DRAGDROP_1003"; + public const string Timeout = "DRAGDROP_1004"; + + // Ошибки источников + public const string SourceCannotDrag = "DRAGDROP_2001"; + public const string SourceStartFailed = "DRAGDROP_2002"; + + // Ошибки целей + public const string TargetNotFound = "DRAGDROP_3001"; + public const string TargetCannotAccept = "DRAGDROP_3002"; + public const string TargetDropFailed = "DRAGDROP_3003"; + + // Ошибки системы + public const string SystemNotInitialized = "DRAGDROP_4001"; + public const string SystemDisposed = "DRAGDROP_4002"; + public const string MemoryAllocationFailed = "DRAGDROP_4003"; +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs b/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d6401ce --- /dev/null +++ b/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +namespace Lattice.Core.DragDrop.Extensions; + +/// +/// Методы расширения для регистрации сервисов перетаскивания. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Добавляет сервис перетаскивания. + /// + /// Коллекция сервисов. + /// Коллекция сервисов. + /// + /// Реализация DI должна быть предоставлена конкретным приложением. + /// + public static object AddDragDropService(this object serviceCollection) + { + // Реализация регистрации сервиса должна быть в конкретном приложении + // Это абстрактный метод для поддержки DI без зависимостей + return serviceCollection; + } + + /// + /// Добавляет сервис перетаскивания с конфигурацией. + /// + /// Коллекция сервисов. + /// Действие конфигурации. + /// Коллекция сервисов. + public static object AddDragDropService( + this object serviceCollection, + Action configure) + { + var options = new DragDropServiceOptions(); + configure(options); + + // Реализация регистрации с опциями должна быть в конкретном приложении + return serviceCollection; + } +} + +/// +/// Опции конфигурации сервиса перетаскивания. +/// +public class DragDropServiceOptions +{ + /// + /// Порог начала перетаскивания в пикселях. + /// + public double DragStartThreshold { get; set; } = 3.0; + + /// + /// Включить ведение журнала операций. + /// + public bool EnableLogging { get; set; } = false; + + /// + /// Включить автоматическую очистку неиспользуемых целей. + /// + public bool EnableAutoCleanup { get; set; } = true; + + /// + /// Интервал автоматической очистки в миллисекундах. + /// + public int AutoCleanupInterval { get; set; } = 60000; + + /// + /// Включить асинхронную обработку операций. + /// + public bool EnableAsyncOperations { get; set; } = true; + + /// + /// Время ожидания асинхронных операций в миллисекундах. + /// + public int AsyncOperationTimeout { get; set; } = 5000; + + /// + /// Включить сбор статистики. + /// + public bool EnableStatistics { get; set; } = true; + + /// + /// Включить проверку типов данных. + /// + public bool EnableTypeChecking { get; set; } = true; +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj b/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj new file mode 100644 index 0000000..c18ab9e --- /dev/null +++ b/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj @@ -0,0 +1,20 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + latest + true + true + Lattice.Core.DragDrop + 1.0.0 + FrigaT + Professional drag-and-drop system for Lattice UI Framework + ui;framework;drag;drop;docking;toolbox + + + + + + diff --git a/Lattice.Core.DragDrop/Models/DragInfo.cs b/Lattice.Core.DragDrop/Models/DragInfo.cs new file mode 100644 index 0000000..6f2d85f --- /dev/null +++ b/Lattice.Core.DragDrop/Models/DragInfo.cs @@ -0,0 +1,227 @@ +using Lattice.Core.Geometry; +using System.Collections.Concurrent; + +namespace Lattice.Core.DragDrop.Models; + +/// +/// Содержит информацию о начале операции перетаскивания. +/// Этот класс передается от источника перетаскивания к системе перетаскивания +/// для инициализации и управления операцией. +/// +/// +/// +/// является ключевым компонентом системы перетаскивания, +/// инкапсулирующим все необходимые данные для начала операции. Он содержит: +/// +/// +/// Данные для передачи +/// Разрешенные эффекты перетаскивания +/// Начальную позицию операции +/// Ссылку на источник перетаскивания +/// Дополнительные параметры операции +/// +/// +/// Этот класс используется как внутренний механизм передачи данных между +/// и системой управления перетаскиванием. +/// +/// +public class DragInfo : IDisposable, ICloneable +{ + private readonly ConcurrentDictionary _parameters = new(); + private bool _disposed; + + /// + /// Получает данные, которые передаются в операции перетаскивания. + /// + /// + /// Объект, содержащий данные для передачи. Может быть любого типа, + /// поддерживаемого системой перетаскивания. + /// + /// + /// Эти данные будут доступны цели сброса через . + /// Важно, чтобы данные были сериализуемыми, если операция перетаскивания + /// может выходить за пределы процесса приложения. + /// + public object Data { get; } + + /// + /// Получает разрешенные эффекты для этой операции перетаскивания. + /// + /// + /// Комбинация флагов , определяющая, + /// какие операции разрешены для этого перетаскивания. + /// + /// + /// Этот параметр используется системой для фильтрации допустимых операций + /// и предоставления соответствующей визуальной обратной связи пользователю. + /// + public Enums.DragDropEffects AllowedEffects { get; } + + /// + /// Получает начальную позицию операции перетаскивания в координатах экрана. + /// + /// + /// Точка в экранных координатах, где была начата операция перетаскивания. + /// + /// + /// Эта позиция используется для вычисления смещения при создании визуального + /// представления перетаскивания и для определения порога начала операции. + /// + public Point StartPosition { get; } + + /// + /// Получает источник перетаскивания, который инициировал операцию. + /// + /// + /// Объект, реализующий , или null, + /// если источник не доступен или не требуется. + /// + /// + /// Эта ссылка может использоваться для уведомления источника о результате + /// операции перетаскивания (завершении или отмене). + /// + public object? Source { get; } + + /// + /// Получает или задает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + /// + /// Словарь, содержащий пары ключ-значение с дополнительными параметрами. + /// + /// + /// Используется для передачи контекстной информации, которая не входит + /// в стандартный набор свойств, но может быть полезной для обработки + /// операции перетаскивания. + /// + public IReadOnlyDictionary Parameters => _parameters; + + /// + /// Инициализирует новый экземпляр класса . + /// + /// + /// Данные, которые передаются в операции перетаскивания. + /// Не может быть null. + /// + /// + /// Разрешенные эффекты для этой операции перетаскивания. + /// + /// + /// Начальная позиция операции перетаскивания в координатах экрана. + /// + /// + /// Источник перетаскивания, который инициировал операцию. Может быть null. + /// + /// + /// Выбрасывается, когда равен null. + /// + /// + /// Конструктор создает экземпляр с указанными + /// параметрами и инициализирует коллекцию параметров пустым словарем. + /// + public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null) + { + Data = data ?? throw new ArgumentNullException(nameof(data)); + AllowedEffects = allowedEffects; + StartPosition = startPosition; + Source = source; + } + + /// + /// Создает новый экземпляр с теми же данными, + /// но новой позицией. + /// + /// + /// Новая позиция для информации о перетаскивании. + /// + /// + /// Новый экземпляр с обновленной позицией. + /// + /// + /// Этот метод используется для обновления информации о перетаскивании + /// при перемещении курсора, сохраняя исходные данные и параметры. + /// + public DragInfo CloneWithPosition(Point newPosition) + { + ThrowIfDisposed(); + + var clone = new DragInfo(Data, AllowedEffects, newPosition, Source); + + foreach (var kvp in _parameters) + { + clone._parameters[kvp.Key] = kvp.Value; + } + + return clone; + } + + /// + /// Создает новый экземпляр с теми же данными. + /// + public DragInfo Clone() => new DragInfo(Data, AllowedEffects, StartPosition, Source); + + /// + object ICloneable.Clone() => this.Clone(); + + /// + /// Получает или дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public T? GetParameter(string key, T? defaultValue = default) + { + if (Parameters.TryGetValue(key, out var value) && value is T typedValue) + { + return typedValue; + } + return defaultValue; + } + + /// + /// Получает или дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public bool TryGetParameter(string key, out T? value) + { + value = default; + + if (_parameters.TryGetValue(key, out var objValue) && objValue is T typedValue) + { + value = typedValue; + return true; + } + + return false; + } + + /// + /// Задает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public void SetParameter(string key, T value) + { + _parameters[key] = value!; + } + + /// + /// Освобождает ресурсы. + /// + public void Dispose() + { + if (_disposed) return; + + _parameters.Clear(); + _disposed = true; + GC.SuppressFinalize(this); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(DragInfo)); + } + + ~DragInfo() + { + Dispose(); + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Models/DropInfo.cs b/Lattice.Core.DragDrop/Models/DropInfo.cs new file mode 100644 index 0000000..1ee05fb --- /dev/null +++ b/Lattice.Core.DragDrop/Models/DropInfo.cs @@ -0,0 +1,269 @@ +using Lattice.Core.DragDrop.Enums; +using Lattice.Core.Geometry; + +namespace Lattice.Core.DragDrop.Models; + +/// +/// Содержит информацию о потенциальном или фактическом сбросе в операции перетаскивания. +/// Этот класс используется для передачи данных между системой перетаскивания +/// и целью сброса (). +/// +/// +/// +/// предоставляет цель сброса всей необходимой информацией +/// для принятия решения о возможности сброса и выполнения соответствующей операции. +/// Ключевые аспекты включают: +/// +/// +/// Предлагаемые для сброса данные +/// Текущую позицию курсора +/// Разрешенные эффекты от источника +/// Предлагаемые эффекты для сброса +/// Ссылку на цель сброса +/// Флаг обработки операции +/// +/// +/// Этот класс является изменяемым, позволяя цели сброса обновлять предлагаемые +/// эффекты и помечать операцию как обработанную. +/// +/// +public class DropInfo +{ + private DragDropEffects _effects = DragDropEffects.None; + public DropPosition DropPosition { get; set; } = DropPosition.Inside; + public bool ShowVisualFeedback { get; set; } = true; + public object? VisualFeedbackData { get; set; } + + /// + /// Получает данные, которые предлагаются для сброса. + /// + /// + /// Данные, переданные от источника перетаскивания, или null, если данные + /// не доступны или операция была отменена. + /// + /// + /// Эти данные соответствуют свойству из + /// исходной информации о перетаскивании. + /// + public object? Data { get; } + + /// + /// Получает текущую позицию курсора в координатах экрана. + /// + /// + /// Точка в экранных координатах, представляющая текущее положение курсора + /// мыши во время операции перетаскивания. + /// + /// + /// Эта позиция используется для определения точного места сброса и может + /// влиять на предлагаемые эффекты (например, различные операции для + /// разных областей цели сброса). + /// + public Point Position { get; } + + /// + /// Получает разрешенные эффекты от источника перетаскивания. + /// + /// + /// Комбинация флагов , определяющая, + /// какие операции разрешил источник. + /// + /// + /// Цель сброса должна уважать эти ограничения и не предлагать эффекты, + /// которые не разрешены источником. + /// + public Enums.DragDropEffects AllowedEffects { get; } + + /// + /// Получает или задает предлагаемые эффекты для операции сброса. + /// + /// + /// Комбинация флагов , предлагаемая + /// целью сброса. По умолчанию равно . + /// + /// + /// + /// Цель сброса должна установить это свойство в методе + /// на основе анализа предоставленных данных и текущего контекста. + /// + /// + /// Если цель не устанавливает это свойство, система перетаскивания + /// будет использовать эффекты по умолчанию. + /// + /// + public Enums.DragDropEffects SuggestedEffects + { + get => _effects; + set => _effects = value; + } + + /// + /// Получает цель сброса, которая обрабатывает эту информацию. + /// + /// + /// Объект, реализующий , или null, + /// если цель не определена. + /// + /// + /// Эта ссылка позволяет системе идентифицировать, какая цель обрабатывает + /// информацию о сбросе, и используется для отслеживания изменений цели + /// во время операции перетаскивания. + /// + public object? Target { get; } + + /// + /// Получает или задает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + /// + /// Словарь, содержащий пары ключ-значение с дополнительными параметрами. + /// + /// + /// Может использоваться для передачи контекстной информации между + /// различными компонентами системы перетаскивания или для хранения + /// временных данных во время обработки операции. + /// + public Dictionary Parameters { get; set; } + + /// + /// Получает значение, указывающее, был ли сброс уже обработан. + /// + /// + /// true, если операция сброса была помечена как обработанная; + /// в противном случае — false. + /// + /// + /// + /// Это свойство используется для предотвращения множественной обработки + /// одной и той же операции сброса. После вызова метода , + /// свойство становится true. + /// + /// + /// Система перетаскивания может проверять это свойство, чтобы определить, + /// нужно ли выполнять дополнительную обработку по умолчанию. + /// + /// + public bool Handled { get; private set; } + + /// + /// Получает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public T? GetParameter(string key, T? defaultValue = default) + { + if (Parameters.TryGetValue(key, out var value) && value is T typedValue) + { + return typedValue; + } + return defaultValue; + } + + /// + /// Получает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public bool TryGetParameter(string key, out T? value) + { + value = default; + + if (Parameters.TryGetValue(key, out var objValue) && objValue is T typedValue) + { + value = typedValue; + return true; + } + + return false; + } + + /// + /// Задает дополнительные параметры, специфичные для конкретной + /// реализации перетаскивания. + /// + public void SetParameter(string key, T value) + { + Parameters[key] = value!; + } + + /// + /// Инициализирует новый экземпляр класса . + /// + /// + /// Данные, которые предлагаются для сброса. Может быть null. + /// + /// + /// Текущая позиция курсора в координатах экрана. + /// + /// + /// Разрешенные эффекты от источника перетаскивания. + /// + /// + /// Цель сброса, которая обрабатывает эту информацию. Может быть null. + /// + /// + /// Конструктор создает экземпляр с указанными + /// параметрами, инициализирует коллекцию параметров пустым словарем + /// и устанавливает флаг в false. + /// + public DropInfo(object? data, Point position, Enums.DragDropEffects allowedEffects, object? target = null) + { + Data = data; + Position = position; + AllowedEffects = allowedEffects; + Target = target; + Parameters = new Dictionary(); + Handled = false; + } + + /// + /// Помечает сброс как обработанный. + /// + /// + /// + /// Этот метод должен вызываться целью сброса в методе , + /// если она успешно обработала операцию сброса. + /// + /// + /// После вызова этого метода свойство становится true, + /// что сигнализирует системе перетаскивания о том, что дополнительная + /// обработка не требуется. + /// + /// + public void MarkAsHandled() + { + Handled = true; + } + + /// + /// Создает новый экземпляр с теми же данными, + /// но новой позицией. + /// + /// + /// Новая позиция для информации о сбросе. + /// + /// + /// Новый экземпляр с обновленной позицией. + /// + /// + /// Этот метод используется для обновления информации о сбросе при + /// перемещении курсора, сохраняя исходные данные и параметры. + /// + public DropInfo WithPosition(Point newPosition) + { + return new DropInfo(Data, newPosition, AllowedEffects, Target) + { + Parameters = new Dictionary(Parameters), + SuggestedEffects = _effects, + DropPosition = DropPosition, + ShowVisualFeedback = ShowVisualFeedback, + VisualFeedbackData = VisualFeedbackData + }; + } + + /// + /// Проверка установки эффекта перетаскивания в разрешенные эффекты. + /// + public bool CanAcceptEffect(Enums.DragDropEffects effect) + { + return (AllowedEffects & effect) != Enums.DragDropEffects.None; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/README.md b/Lattice.Core.DragDrop/README.md new file mode 100644 index 0000000..80849b0 --- /dev/null +++ b/Lattice.Core.DragDrop/README.md @@ -0,0 +1,832 @@ +# Lattice.Core.DragDrop + +Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности. + +## 📋 Особенности + +- ✅ **Полная асинхронная поддержка** - async/await для всех операций +- ✅ **Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации +- ✅ **Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации +- ✅ **Расширяемость** - Легкая интеграция с любыми UI фреймворками +- ✅ **Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов +- ✅ **Статистика** - Встроенный мониторинг производительности + +## 🏗️ Архитектура + +### Основные компоненты + +``` +Lattice.Core.DragDrop/ +├── Abstractions/ # Интерфейсы +│ ├── IDragSource.cs # Источник перетаскивания (синхронный) +│ ├── IAsyncDragSource.cs # Асинхронный источник +│ ├── IDropTarget.cs # Цель сброса (синхронная) +│ └── IAsyncDropTarget.cs # Асинхронная цель +├── Enums/ # Перечисления +├── Exceptions/ # Исключения с кодами ошибок +├── Extensions/ # Расширения для DI +├── Models/ # Модели данных +│ ├── DragInfo.cs # Информация о перетаскивании +│ └── DropInfo.cs # Информация о сбросе +├── Services/ # Сервисы +│ ├── IDragDropService.cs # Основной интерфейс +│ ├── DragDropService.cs # Реализация сервиса +│ └── EventArgs/ # Аргументы событий +└── Utilities/ # Утилиты и фабрики + ├── DragDropUtilities.cs # Синхронные утилиты + └── AsyncDragDropUtilities.cs # Асинхронные утилиты +``` + +## 🚀 Быстрый старт + +### 1. Установка + +```csharp +// Добавьте проект Lattice.Core.DragDrop в ваше решение +// или создайте NuGet пакет +``` + +### 2. Базовое использование + +```csharp +using Lattice.Core.DragDrop; +using Lattice.Core.DragDrop.Abstractions; +using Lattice.Core.DragDrop.Services; +using Lattice.Core.Geometry; + +// Создаем сервис +var dragDropService = new DragDropService(); + +// Создаем простой источник перетаскивания +var dragSource = DragDropUtilities.CreateSimpleDragSource( + dataProvider: () => "Example Data", + canDrag: () => true, + onCompleted: (dragInfo, effects) => + Console.WriteLine($"Drag completed with effects: {effects}"), + onCancelled: dragInfo => + Console.WriteLine("Drag cancelled") +); + +// Создаем простую цель сброса +var dropTarget = DragDropUtilities.CreateSimpleDropTarget( + canAccept: dropInfo => dropInfo.Data is string, + onDragOver: dropInfo => + dropInfo.SuggestedEffects = DragDropEffects.Copy, + onDrop: dropInfo => + { + Console.WriteLine($"Dropped: {dropInfo.Data}"); + dropInfo.MarkAsHandled(); + } +); + +// Регистрируем цель +string targetId = dragDropService.RegisterDropTarget( + dropTarget, + new Rect(100, 100, 300, 200) +); + +// Начинаем перетаскивание +bool started = dragDropService.StartDrag( + dragSource, + new Point(50, 50) +); + +if (started) +{ + // Обновляем позицию + dragDropService.UpdateDrag(new Point(150, 150)); + + // Завершаем + var effects = dragDropService.EndDrag(new Point(200, 200)); +} +``` + +## 📖 Подробное руководство + +### Сервис перетаскивания + +Основной класс системы - `DragDropService`, реализующий `IDragDropService`. + +```csharp +// Создание с кастомными настройками +var service = new DragDropService(options => +{ + options.DragStartThreshold = 5.0; + options.EnableAsyncOperations = true; + options.AsyncOperationTimeout = 3000; + options.EnableAutoCleanup = true; +}); + +// Свойства +bool isActive = service.IsDragActive; // Активна ли операция +DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация +double threshold = service.DragStartThreshold; // Порог начала + +// События +service.DragStarted += OnDragStarted; +service.DragUpdated += OnDragUpdated; +service.DragCompleted += OnDragCompleted; +service.DragCancelled += OnDragCancelled; +service.ErrorOccurred += OnErrorOccurred; + +// Регистрация целей +string id = service.RegisterDropTarget( + target, // IDropTarget + bounds, // Rect + priority: 1, // Приоритет (выше = выше приоритет) + group: "main" // Группа для группового удаления +); + +// Обновление границ +service.UpdateDropTargetBounds(id, newBounds); + +// Удаление +service.UnregisterDropTarget(id); +service.UnregisterDropTargetsInGroup("main"); +``` + +### Асинхронное использование + +```csharp +// Асинхронные методы +bool started = await service.StartDragAsync(source, startPosition); +await service.UpdateDragAsync(currentPosition); +DragDropEffects effects = await service.EndDragAsync(dropPosition); +await service.CancelDragAsync(); + +// Статистика +var stats = service.GetStats(); +Console.WriteLine($"Operations: {stats.TotalDragOperations}"); +Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}"); +Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms"); +``` + +### Создание кастомных источников и целей + +#### Синхронная реализация + +```csharp +public class FileDragSource : IDragSource +{ + private readonly FileInfo _file; + + public FileDragSource(FileInfo file) => _file = file; + + public bool CanStartDrag(out DragInfo? dragInfo) + { + // Проверяем условия + if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit + { + dragInfo = null; + return false; + } + + // Создаем DragInfo + dragInfo = new DragInfo( + data: _file, + allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, + startPosition: Point.Zero, + source: this + ); + + // Добавляем дополнительные параметры + dragInfo.SetParameter("FileSize", _file.Length); + dragInfo.SetParameter("MimeType", GetMimeType(_file)); + + return true; + } + + public bool StartDrag(DragInfo dragInfo) + { + // Подготовка к перетаскиванию + // Можно создать визуальное представление и т.д. + Console.WriteLine($"Starting drag of {_file.Name}"); + return true; + } + + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + Console.WriteLine($"File drag completed with {effects}"); + + if (effects == DragDropEffects.Move) + { + // Файл был перемещен - возможно, удалить оригинал + // _file.Delete(); + } + } + + public void DragCancelled(DragInfo dragInfo) + { + Console.WriteLine("File drag cancelled"); + } +} +``` + +#### Асинхронная реализация + +```csharp +public class DatabaseItemDragSource : IAsyncDragSource +{ + private readonly DatabaseService _db; + private readonly int _itemId; + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + try + { + // Асинхронные проверки + var canDrag = await _db.CanDragItemAsync(_itemId); + if (!canDrag) return (false, null); + + // Асинхронная загрузка данных + var data = await _db.GetItemForDragAsync(_itemId); + if (data == null) return (false, null); + + var dragInfo = new DragInfo( + data: data, + allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, + startPosition: Point.Zero, + source: this + ); + + return (true, dragInfo); + } + catch (Exception ex) + { + // Логирование ошибки + return (false, null); + } + } + + public Task StartDragAsync(DragInfo dragInfo) + { + // Асинхронная подготовка + return Task.FromResult(true); + } + + public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects) + { + // Асинхронная обработка завершения + await _db.LogDragOperationAsync(_itemId, effects); + + if (effects == DragDropEffects.Move) + { + await _db.MarkItemAsMovedAsync(_itemId); + } + } + + public Task DragCancelledAsync(DragInfo dragInfo) + { + return Task.CompletedTask; + } + + // Синхронные методы для совместимости + public bool CanStartDrag(out DragInfo? dragInfo) + { + var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult(); + dragInfo = result.DragInfo; + return result.CanStart; + } + + // ... остальные синхронные методы +} +``` + +### Работа с моделями данных + +#### DragInfo + +```csharp +// Создание +var dragInfo = new DragInfo( + data: myObject, + allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, + startPosition: new Point(x, y), + source: this +); + +// Параметры +dragInfo.SetParameter("Timestamp", DateTime.UtcNow); +dragInfo.SetParameter("UserId", currentUser.Id); + +// Получение параметров +if (dragInfo.TryGetParameter("Category", out var category)) +{ + // Используем категорию +} + +// Клонирование с новой позицией +var updatedDragInfo = dragInfo.CloneWithPosition(newPosition); + +// Очистка ресурсов +dragInfo.Dispose(); +``` + +#### DropInfo + +```csharp +// Создается сервисом автоматически +// Работа с DropInfo в методах цели: + +public void DragOver(DropInfo dropInfo) +{ + // Проверяем данные + if (dropInfo.Data is MyDataType myData) + { + // Определяем позицию относительно цели + dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position); + + // Предлагаем эффекты + if (CanAcceptData(myData)) + { + dropInfo.SuggestedEffects = DragDropEffects.Move; + dropInfo.ShowVisualFeedback = true; + dropInfo.VisualFeedbackData = CreatePreview(myData); + } + else + { + dropInfo.SuggestedEffects = DragDropEffects.None; + } + } +} + +public void Drop(DropInfo dropInfo) +{ + if (dropInfo.Data is MyDataType myData) + { + // Обработка сброса + ProcessDrop(myData, dropInfo.DropPosition); + + // Помечаем как обработанное + dropInfo.MarkAsHandled(); + } +} +``` + +### Утилиты и фабрики + +#### Синхронные утилиты + +```csharp +// Простые реализации +var simpleSource = DragDropUtilities.CreateSimpleDragSource( + () => data, + () => true, + (dragInfo, effects) => Console.WriteLine("Completed"), + dragInfo => Console.WriteLine("Cancelled") +); + +var simpleTarget = DragDropUtilities.CreateSimpleDropTarget( + dropInfo => dropInfo.Data != null, + dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy, + dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"), + () => Console.WriteLine("Drag left") +); + +// Геометрия +double distance = DragDropUtilities.CalculateDistance(p1, p2); +bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold); +DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold); + +// Проверка совместимости +bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects); +bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) }); +``` + +#### Асинхронные утилиты + +```csharp +// Асинхронные реализации +var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource( + async () => await LoadDataAsync(), + async () => await CanDragAsync(), + async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects), + async dragInfo => await OnCancelledAsync(dragInfo) +); + +var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget( + async dropInfo => await CanAcceptAsync(dropInfo.Data), + async dropInfo => await OnDragOverAsync(dropInfo), + async dropInfo => await OnDropAsync(dropInfo), + async () => await OnDragLeaveAsync() +); + +// Адаптеры для синхронных интерфейсов +IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource); +IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget); + +// Комбинированные реализации (fallback стратегия) +var combined = AsyncDragDropUtilities.Combine( + syncSource, + asyncSource, + preferAsync: true // При ошибке в async использует sync +); + +// Таймауты +var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync( + task: LongOperationAsync(), + timeout: TimeSpan.FromSeconds(5), + defaultValue: fallbackValue +); +``` + +### Обработка ошибок + +```csharp +// Подписка на ошибки +service.ErrorOccurred += (sender, e) => +{ + Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}"); + + // Коды ошибок определены в DragDropErrorCodes + switch (e.ErrorCode) + { + case DragDropErrorCodes.Timeout: + Console.WriteLine("Operation timed out"); + break; + case DragDropErrorCodes.SourceCannotDrag: + Console.WriteLine("Source cannot drag"); + break; + case DragDropErrorCodes.TargetCannotAccept: + Console.WriteLine("Target cannot accept"); + break; + } +}; + +// Использование в коде +try +{ + await service.StartDragAsync(source, position); +} +catch (DragDropException ex) +{ + // Обработка специфичных для DragDrop ошибок + Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}"); +} +catch (Exception ex) +{ + // Обработка других ошибок + Console.WriteLine($"General error: {ex.Message}"); +} +``` + +## 🔧 Интеграция с UI фреймворками + +### Базовый адаптер для WinUI/WPF + +```csharp +public class UIElementDragSource : IAsyncDragSource +{ + private readonly FrameworkElement _element; + private readonly Func _dataProvider; + + public UIElementDragSource(FrameworkElement element, Func dataProvider) + { + _element = element; + _dataProvider = dataProvider; + + // Подписка на события + _element.PointerPressed += OnPointerPressed; + _element.PointerMoved += OnPointerMoved; + _element.PointerReleased += OnPointerReleased; + } + + private Point _dragStartPosition; + private bool _isDragging; + + private void OnPointerPressed(object sender, PointerRoutedEventArgs e) + { + var point = e.GetCurrentPoint(_element); + _dragStartPosition = new Point(point.Position.X, point.Position.Y); + } + + private async void OnPointerMoved(object sender, PointerRoutedEventArgs e) + { + if (_isDragging) return; + + var point = e.GetCurrentPoint(_element); + var current = new Point(point.Position.X, point.Position.Y); + + var distance = Math.Sqrt( + Math.Pow(current.X - _dragStartPosition.X, 2) + + Math.Pow(current.Y - _dragStartPosition.Y, 2)); + + if (distance > 3.0) // Порог + { + _isDragging = true; + + // Начинаем перетаскивание через сервис + var service = GetDragDropService(); + await service.StartDragAsync(this, ConvertToScreen(_dragStartPosition)); + } + } + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + var data = _dataProvider(); + if (data == null) return (false, null); + + var dragInfo = new DragInfo( + data, + DragDropEffects.Copy | DragDropEffects.Move, + Point.Zero, + this + ); + + return (true, dragInfo); + } + + // ... остальная реализация +} +``` + +## 🧪 Тестирование + +### Примеры модульных тестов + +```csharp +[TestClass] +public class DragDropServiceTests +{ + private DragDropService _service; + private Mock _mockSource; + private Mock _mockTarget; + + [TestInitialize] + public void Setup() + { + _service = new DragDropService(); + _mockSource = new Mock(); + _mockTarget = new Mock(); + } + + [TestMethod] + public async Task StartDrag_ValidSource_ReturnsTrue() + { + // Arrange + var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero); + _mockSource.Setup(s => s.CanStartDragAsync()) + .ReturnsAsync((true, dragInfo)); + _mockSource.Setup(s => s.StartDragAsync(It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _service.StartDragAsync(_mockSource.Object, Point.Zero); + + // Assert + Assert.IsTrue(result); + Assert.IsTrue(_service.IsDragActive); + } + + [TestMethod] + public async Task UpdateDrag_FindsTarget_CallsDragOver() + { + // Arrange + var targetId = _service.RegisterDropTarget( + _mockTarget.Object, + new Rect(0, 0, 100, 100) + ); + + await StartTestDrag(); + + _mockTarget.Setup(t => t.CanAcceptDropAsync(It.IsAny())) + .ReturnsAsync(true); + + // Act + await _service.UpdateDragAsync(new Point(50, 50)); + + // Assert + _mockTarget.Verify(t => t.DragOverAsync(It.IsAny()), Times.Once); + } + + private async Task StartTestDrag() + { + var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero); + _mockSource.Setup(s => s.CanStartDragAsync()) + .ReturnsAsync((true, dragInfo)); + _mockSource.Setup(s => s.StartDragAsync(It.IsAny())) + .ReturnsAsync(true); + + await _service.StartDragAsync(_mockSource.Object, Point.Zero); + } +} +``` + +## 📊 Мониторинг и производительность + +### Сбор статистики + +```csharp +// Получение статистики +var stats = service.GetStats(); + +Console.WriteLine($"Total operations: {stats.TotalDragOperations}"); +Console.WriteLine($"Successful: {stats.SuccessfulDrops}"); +Console.WriteLine($"Cancelled: {stats.CancelledOperations}"); +Console.WriteLine($"Errors: {stats.ErrorCount}"); +Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms"); + +// Мониторинг в реальном времени +private Stopwatch _operationTimer; + +service.DragStarted += (s, e) => +{ + _operationTimer = Stopwatch.StartNew(); + Console.WriteLine($"Drag started from {e.DragInfo.Source}"); +}; + +service.DragCompleted += (s, e) => +{ + _operationTimer.Stop(); + Console.WriteLine($"Drag completed in {_operationTimer.ElapsedMilliseconds}ms"); + + if (service.EnableAsyncOperations) + { + var stats = service.GetStats(); + Console.WriteLine($"Success rate: {(double)stats.SuccessfulDrops / stats.TotalDragOperations:P}"); + } +}; +``` + +### Оптимизация производительности + +```csharp +// 1. Настройка параметров +var service = new DragDropService(options => +{ + options.DragStartThreshold = 4.0; // Увеличить порог для предотвращения случайных перетаскиваний + options.AsyncOperationTimeout = 2000; // Уменьшить таймаут для отзывчивости + options.EnableAutoCleanup = true; // Автоочистка неиспользуемых целей +}); + +// 2. Группировка целей +_service.RegisterDropTarget(target1, bounds1, group: "toolbox"); +_service.RegisterDropTarget(target2, bounds2, group: "toolbox"); +// Быстрое удаление всех целей группы +_service.UnregisterDropTargetsInGroup("toolbox"); + +// 3. Приоритеты для оптимизации поиска +_service.RegisterDropTarget(importantTarget, bounds, priority: 100); // Высокий приоритет +_service.RegisterDropTarget(defaultTarget, bounds, priority: 0); // Низкий приоритет + +// 4. Периодическая очистка +service.ClearAllDropTargets(); // При смене контекста +``` + +## 🚀 Продвинутые сценарии + +### Переупорядочивание элементов + +```csharp +public class ReorderableListDropTarget : IAsyncDropTarget +{ + private readonly IList _items; + + public async Task CanAcceptDropAsync(DropInfo dropInfo) + { + return dropInfo.Data is object && _items.Contains(dropInfo.Data); + } + + public async Task DropAsync(DropInfo dropInfo) + { + var item = dropInfo.Data; + var insertIndex = CalculateInsertIndex(dropInfo); + + // Удаляем из старой позиции + _items.Remove(item); + + // Вставляем в новую позицию + if (insertIndex < _items.Count) + _items.Insert(insertIndex, item); + else + _items.Add(item); + + dropInfo.MarkAsHandled(); + } + + private int CalculateInsertIndex(DropInfo dropInfo) + { + // Логика определения позиции вставки на основе dropInfo.Position + // и визуального расположения элементов + return 0; + } +} +``` + +### Мультиселект и групповое перетаскивание + +```csharp +public class MultiSelectionDragSource : IAsyncDragSource +{ + private readonly IEnumerable _selectedItems; + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + if (!_selectedItems.Any()) return (false, null); + + // Создаем коллекцию для перетаскивания + var dragData = new DragItemCollection(_selectedItems); + + var dragInfo = new DragInfo( + dragData, + DragDropEffects.Copy | DragDropEffects.Move, + Point.Zero, + this + ); + + dragInfo.SetParameter("ItemCount", _selectedItems.Count()); + dragInfo.SetParameter("IsMultiSelect", true); + + return (true, dragInfo); + } +} +``` + +## 📚 API Reference + +### Основные интерфейсы + +#### IDragDropService +```csharp +bool IsDragActive { get; } +DragInfo? CurrentDragInfo { get; } +IDropTarget? CurrentDropTarget { get; } +double DragStartThreshold { get; set; } +bool EnableAsyncOperations { get; set; } + +// Регистрация целей +string RegisterDropTarget(IDropTarget target, Rect bounds, int priority = 0, string? group = null); +bool UpdateDropTargetBounds(string id, Rect bounds); +bool UnregisterDropTarget(string id); +void UnregisterDropTargetsInGroup(string group); + +// Асинхронные операции +Task StartDragAsync(IDragSource source, Point startPosition); +Task UpdateDragAsync(Point position); +Task EndDragAsync(Point position); +Task CancelDragAsync(); + +// Синхронные операции +bool StartDrag(IDragSource source, Point startPosition); +void UpdateDrag(Point position); +DragDropEffects EndDrag(Point position); +void CancelDrag(); + +// Утилиты +void ClearAllDropTargets(); +DragDropStats GetStats(); +``` + +#### IAsyncDragSource +```csharp +Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync(); +Task StartDragAsync(DragInfo dragInfo); +Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects); +Task DragCancelledAsync(DragInfo dragInfo); +``` + +#### IAsyncDropTarget +```csharp +Task CanAcceptDropAsync(DropInfo dropInfo); +Task DragOverAsync(DropInfo dropInfo); +Task DropAsync(DropInfo dropInfo); +Task DragLeaveAsync(); +``` + +### Перечисления + +#### DragDropEffects +```csharp +[Flags] +None = 0 +Copy = 1 << 0 // Копирование данных +Move = 1 << 1 // Перемещение данных +Link = 1 << 2 // Ссылка на данные +CopyOrMove = Copy | Move +All = Copy | Move | Link + +// Методы расширения: +bool CanCopy(this DragDropEffects effects) +bool CanMove(this DragDropEffects effects) +bool CanLink(this DragDropEffects effects) +DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey) +``` + +#### DropPosition +```csharp +Inside // Внутри элемента +Top // Сверху +Bottom // Снизу +Left // Слева +Right // Справа +Center // По центру +``` + +## 🔮 Планы развития + +1. **Интеграция с популярными UI фреймворками** (WinUI, Uno Platform, Avalonia) +2. **Поддержка жестов** (тач, мультитач) +3. **Виртуализация** для работы с большими наборами данных +4. **Продвинутые визуальные эффекты** (анимации, превью) +5. **Source Generators** для автоматической генерации кода +6. **Инструменты разработчика** (дебаггер, профилировщик) \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/DragDropService.cs b/Lattice.Core.DragDrop/Services/DragDropService.cs new file mode 100644 index 0000000..4e21943 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/DragDropService.cs @@ -0,0 +1,829 @@ +namespace Lattice.Core.DragDrop.Services; + +/// +/// Реализация сервиса управления операциями перетаскивания. +/// Полностью потокобезопасная реализация с поддержкой async/await. +/// +public sealed class DragDropService : IDragDropService +{ + #region Nested Types + + private sealed class DropTargetInfo : IDisposable + { + public required Abstractions.IDropTarget Target { get; init; } + public required Geometry.Rect Bounds { get; set; } + public required int Priority { get; init; } + public required string? Group { get; init; } + public required string Id { get; init; } + public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; + public int UsageCount { get; set; } + + public void Dispose() + { + if (Target is IDisposable disposable) + disposable.Dispose(); + } + } + + private sealed class DragOperationContext : IDisposable + { + public Abstractions.IDragSource? Source { get; set; } + public Models.DragInfo? DragInfo { get; set; } + public Abstractions.IDropTarget? CurrentDropTarget { get; set; } + public CancellationTokenSource? CancellationTokenSource { get; set; } + public DateTime StartTime { get; set; } = DateTime.UtcNow; + public bool ThresholdExceeded { get; set; } + + public void Dispose() + { + DragInfo?.Dispose(); + CancellationTokenSource?.Dispose(); + } + } + + #endregion + + #region Fields + + private readonly Dictionary _dropTargets = new(); + private readonly ReaderWriterLockSlim _dropTargetsLock = new(LockRecursionPolicy.NoRecursion); + private readonly object _dragOperationLock = new(); + + private DragOperationContext? _currentDragOperation; + private Timer? _cleanupTimer; + private bool _disposed; + + private int _totalDragOperations; + private int _successfulDrops; + private int _cancelledOperations; + private int _errorCount; + private long _totalOperationTicks; + + #endregion + + #region Events + + private event EventHandler? _dragStarted; + private event EventHandler? _dragUpdated; + private event EventHandler? _dropTargetChanged; + private event EventHandler? _dragCompleted; + private event EventHandler? _dragCancelled; + private event EventHandler? _errorOccurred; + + #endregion + + #region Properties + + public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null; + + public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo; + + public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget; + + public double DragStartThreshold { get; set; } = 3.0; + + public bool EnableAsyncOperations { get; set; } = true; + + public int AsyncOperationTimeout { get; set; } = 5000; + + public event EventHandler DragStarted + { + add => _dragStarted += value; + remove => _dragStarted -= value; + } + + public event EventHandler DragUpdated + { + add => _dragUpdated += value; + remove => _dragUpdated -= value; + } + + public event EventHandler DropTargetChanged + { + add => _dropTargetChanged += value; + remove => _dropTargetChanged -= value; + } + + public event EventHandler DragCompleted + { + add => _dragCompleted += value; + remove => _dragCompleted -= value; + } + + public event EventHandler DragCancelled + { + add => _dragCancelled += value; + remove => _dragCancelled -= value; + } + + public event EventHandler ErrorOccurred + { + add => _errorOccurred += value; + remove => _errorOccurred -= value; + } + + #endregion + + #region Constructor + + public DragDropService() + { + // Инициализация таймера очистки (каждые 5 минут) + _cleanupTimer = new Timer(CleanupExpiredTargets, null, + TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + } + + #endregion + + #region Registration Methods + + public string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null) + { + ThrowIfDisposed(); + if (target == null) throw new ArgumentNullException(nameof(target)); + + var id = Guid.NewGuid().ToString("N"); + var info = new DropTargetInfo + { + Target = target, + Bounds = bounds, + Priority = priority, + Group = group, + Id = id + }; + + _dropTargetsLock.EnterWriteLock(); + try + { + _dropTargets[id] = info; + return id; + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + + public bool UpdateDropTargetBounds(string id, Geometry.Rect bounds) + { + ThrowIfDisposed(); + + _dropTargetsLock.EnterUpgradeableReadLock(); + try + { + if (!_dropTargets.TryGetValue(id, out var info)) + return false; + + _dropTargetsLock.EnterWriteLock(); + try + { + info.Bounds = bounds; + info.LastAccessTime = DateTime.UtcNow; + return true; + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + finally + { + _dropTargetsLock.ExitUpgradeableReadLock(); + } + } + + public bool UnregisterDropTarget(string id) + { + ThrowIfDisposed(); + + _dropTargetsLock.EnterWriteLock(); + try + { + if (_dropTargets.Remove(id, out var info)) + { + info.Dispose(); + return true; + } + return false; + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + + public void UnregisterDropTargetsInGroup(string group) + { + ThrowIfDisposed(); + if (string.IsNullOrEmpty(group)) return; + + _dropTargetsLock.EnterWriteLock(); + try + { + var idsToRemove = new List(); + + foreach (var kvp in _dropTargets) + { + if (kvp.Value.Group == group) + { + idsToRemove.Add(kvp.Key); + } + } + + foreach (var id in idsToRemove) + { + if (_dropTargets.Remove(id, out var info)) + { + info.Dispose(); + } + } + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + + #endregion + + #region Async Operations + + public async Task StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition) + { + ThrowIfDisposed(); + if (source == null) throw new ArgumentNullException(nameof(source)); + + lock (_dragOperationLock) + { + if (_currentDragOperation != null) + return false; + } + + try + { + Interlocked.Increment(ref _totalDragOperations); + + Models.DragInfo? dragInfo = null; + + // Проверка возможности начала перетаскивания + if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource) + { + var result = await ExecuteWithTimeoutAsync( + asyncSource.CanStartDragAsync(), + "CanStartDragAsync", + source); + + if (!result.CanStart || result.DragInfo == null) + return false; + + dragInfo = result.DragInfo; + } + else + { + if (!source.CanStartDrag(out dragInfo) || dragInfo == null) + return false; + } + + var updatedDragInfo = dragInfo.CloneWithPosition(startPosition); + + // Начало перетаскивания + bool started; + + if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource2) + { + started = await ExecuteWithTimeoutAsync( + asyncSource2.StartDragAsync(updatedDragInfo), + "StartDragAsync", + source); + } + else + { + started = source.StartDrag(updatedDragInfo); + } + + if (!started) + { + updatedDragInfo.Dispose(); + return false; + } + + lock (_dragOperationLock) + { + _currentDragOperation = new DragOperationContext + { + Source = source, + DragInfo = updatedDragInfo, + CancellationTokenSource = new CancellationTokenSource() + }; + } + + // Вызов события + _dragStarted?.Invoke(this, new DragStartedEventArgs(updatedDragInfo, startPosition)); + return true; + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, "StartDragAsync", source); + return false; + } + } + + public async Task UpdateDragAsync(Geometry.Point position) + { + ThrowIfDisposed(); + + DragOperationContext? context; + lock (_dragOperationLock) + { + context = _currentDragOperation; + } + + if (context?.DragInfo == null || context.Source == null) + return; + + try + { + // Проверка порога начала + if (!context.ThresholdExceeded && DragStartThreshold > 0) + { + var distance = CalculateDistance(context.DragInfo.StartPosition, position); + if (distance < DragStartThreshold) + return; + + context.ThresholdExceeded = true; + } + + var updatedDragInfo = context.DragInfo.CloneWithPosition(position); + context.DragInfo.Dispose(); + context.DragInfo = updatedDragInfo; + + // Поиск новой цели сброса + var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo); + + // Обработка смены цели + if (context.CurrentDropTarget != newDropTarget?.Target) + { + if (context.CurrentDropTarget != null) + { + await ExecuteTargetOperationAsync( + context.CurrentDropTarget, + t => t.DragLeaveAsync(), + t => t.DragLeave(), + "DragLeave"); + } + + context.CurrentDropTarget = newDropTarget?.Target; + + if (newDropTarget != null) + { + newDropTarget.UsageCount++; + _dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs( + updatedDragInfo, newDropTarget.Target, newDropTarget.Bounds)); + } + } + + // Уведомление текущей цели + if (context.CurrentDropTarget != null) + { + var dropInfo = new Models.DropInfo( + updatedDragInfo.Data, + position, + updatedDragInfo.AllowedEffects, + context.CurrentDropTarget); + + await ExecuteTargetOperationAsync( + context.CurrentDropTarget, + t => t.DragOverAsync(dropInfo), + t => t.DragOver(dropInfo), + "DragOver"); + } + + _dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position)); + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, "UpdateDragAsync", context); + } + } + + public async Task EndDragAsync(Geometry.Point position) + { + ThrowIfDisposed(); + + DragOperationContext? context; + lock (_dragOperationLock) + { + context = _currentDragOperation; + _currentDragOperation = null; + } + + if (context == null || context.DragInfo == null || context.Source == null) + { + Reset(); + return Enums.DragDropEffects.None; + } + + try + { + var effects = Enums.DragDropEffects.None; + var operationTime = DateTime.UtcNow - context.StartTime; + Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks); + + // Выполнение сброса + if (context.CurrentDropTarget != null) + { + var dropInfo = new Models.DropInfo( + context.DragInfo.Data, + position, + context.DragInfo.AllowedEffects, + context.CurrentDropTarget); + + await ExecuteTargetOperationAsync( + context.CurrentDropTarget, + t => t.DropAsync(dropInfo), + t => t.Drop(dropInfo), + "Drop"); + + if (dropInfo.Handled) + { + effects = dropInfo.SuggestedEffects; + Interlocked.Increment(ref _successfulDrops); + } + } + + // Уведомление источника + await ExecuteSourceOperationAsync( + context.Source, + s => s.DragCompletedAsync(context.DragInfo, effects), + s => s.DragCompleted(context.DragInfo, effects), + "DragCompleted", + effects); + + // Событие завершения + _dragCompleted?.Invoke(this, new DragCompletedEventArgs( + context.DragInfo, position, effects)); + + return effects; + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, "EndDragAsync", context); + return Enums.DragDropEffects.None; + } + finally + { + context.Dispose(); + } + } + + public async Task CancelDragAsync() + { + ThrowIfDisposed(); + + DragOperationContext? context; + lock (_dragOperationLock) + { + context = _currentDragOperation; + _currentDragOperation = null; + } + + if (context == null || context.DragInfo == null || context.Source == null) + { + Reset(); + return; + } + + try + { + context.CancellationTokenSource?.Cancel(); + Interlocked.Increment(ref _cancelledOperations); + + await ExecuteSourceOperationAsync( + context.Source, + s => s.DragCancelledAsync(context.DragInfo), + s => s.DragCancelled(context.DragInfo), + "DragCancelled"); + + _dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo)); + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, "CancelDragAsync", context); + } + finally + { + context.Dispose(); + } + } + + #endregion + + #region Synchronous Operations (for compatibility) + + public bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition) + { + return Task.Run(() => StartDragAsync(source, startPosition)).GetAwaiter().GetResult(); + } + + public void UpdateDrag(Geometry.Point position) + { + Task.Run(() => UpdateDragAsync(position)).GetAwaiter().GetResult(); + } + + public Enums.DragDropEffects EndDrag(Geometry.Point position) + { + return Task.Run(() => EndDragAsync(position)).GetAwaiter().GetResult(); + } + + public void CancelDrag() + { + Task.Run(CancelDragAsync).GetAwaiter().GetResult(); + } + + #endregion + + #region Utility Methods + + public void ClearAllDropTargets() + { + ThrowIfDisposed(); + + _dropTargetsLock.EnterWriteLock(); + try + { + foreach (var info in _dropTargets.Values) + { + info.Dispose(); + } + _dropTargets.Clear(); + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + + public DragDropStats GetStats() + { + return new DragDropStats + { + TotalDragOperations = _totalDragOperations, + SuccessfulDrops = _successfulDrops, + CancelledOperations = _cancelledOperations, + ErrorCount = _errorCount, + RegisteredTargets = _dropTargets.Count, + AverageOperationTime = _totalDragOperations > 0 + ? TimeSpan.FromTicks(_totalOperationTicks / _totalDragOperations) + : TimeSpan.Zero + }; + } + + #endregion + + #region Private Helper Methods + + private async Task FindDropTargetAsync(Geometry.Point position, Models.DragInfo dragInfo) + { + DropTargetInfo? bestTarget = null; + + _dropTargetsLock.EnterReadLock(); + try + { + foreach (var info in _dropTargets.Values) + { + if (!info.Bounds.Contains(position)) + continue; + + var dropInfo = new Models.DropInfo( + dragInfo.Data, + position, + dragInfo.AllowedEffects, + info.Target); + + bool canAccept; + + if (EnableAsyncOperations && info.Target is Abstractions.IAsyncDropTarget asyncTarget) + { + canAccept = await ExecuteWithTimeoutAsync( + asyncTarget.CanAcceptDropAsync(dropInfo), + "CanAcceptDropAsync", + info.Target); + } + else + { + canAccept = info.Target.CanAcceptDrop(dropInfo); + } + + if (canAccept) + { + info.LastAccessTime = DateTime.UtcNow; + + if (bestTarget == null || info.Priority > bestTarget.Priority) + { + bestTarget = info; + } + } + } + } + finally + { + _dropTargetsLock.ExitReadLock(); + } + + return bestTarget; + } + + private async Task ExecuteTargetOperationAsync( + Abstractions.IDropTarget target, + Func asyncOperation, + Action syncOperation, + string operationName) + { + try + { + if (EnableAsyncOperations && target is Abstractions.IAsyncDropTarget asyncTarget) + { + await ExecuteWithTimeoutAsync( + asyncOperation(asyncTarget), + $"{operationName}Async", + target); + } + else + { + syncOperation(target); + } + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, operationName, target); + } + } + + private async Task ExecuteSourceOperationAsync( + Abstractions.IDragSource source, + Func asyncOperation, + Action syncOperation, + string operationName, + Enums.DragDropEffects effects = Enums.DragDropEffects.None) + { + try + { + if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource) + { + await ExecuteWithTimeoutAsync( + asyncOperation(asyncSource), + $"{operationName}Async", + source); + } + else + { + syncOperation(source); + } + } + catch (Exception ex) + { + Interlocked.Increment(ref _errorCount); + HandleError(ex, operationName, source); + } + } + + private async Task ExecuteWithTimeoutAsync(Task task, string operationName, object? context = null) + { + if (AsyncOperationTimeout <= 0) + return await task; + + var timeoutTask = Task.Delay(AsyncOperationTimeout); + var completedTask = await Task.WhenAny(task, timeoutTask); + + if (completedTask == timeoutTask) + { + throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms"); + } + + return await task; + } + + private async Task ExecuteWithTimeoutAsync(Task task, string operationName, object? context = null) + { + if (AsyncOperationTimeout <= 0) + { + await task; + return; + } + + var timeoutTask = Task.Delay(AsyncOperationTimeout); + var completedTask = await Task.WhenAny(task, timeoutTask); + + if (completedTask == timeoutTask) + { + throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms"); + } + + await task; + } + + private double CalculateDistance(Geometry.Point p1, Geometry.Point p2) + { + var dx = p2.X - p1.X; + var dy = p2.Y - p1.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private void Reset() + { + lock (_dragOperationLock) + { + _currentDragOperation?.Dispose(); + _currentDragOperation = null; + } + } + + private void CleanupExpiredTargets(object? state) + { + var expirationTime = DateTime.UtcNow.AddMinutes(-10); // Цели старше 10 минут + + _dropTargetsLock.EnterWriteLock(); + try + { + var idsToRemove = new List(); + + foreach (var kvp in _dropTargets) + { + if (kvp.Value.LastAccessTime < expirationTime) + { + idsToRemove.Add(kvp.Key); + } + } + + foreach (var id in idsToRemove) + { + if (_dropTargets.Remove(id, out var info)) + { + info.Dispose(); + } + } + } + finally + { + _dropTargetsLock.ExitWriteLock(); + } + } + + private void HandleError(Exception exception, string operation, object? context = null) + { + _errorOccurred?.Invoke(this, new DragDropErrorEventArgs(exception, operation, context)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + } + + #endregion + + #region IDisposable Implementation + + public void Dispose() + { + if (_disposed) return; + + lock (_dragOperationLock) + { + if (_disposed) return; + + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + + if (_currentDragOperation != null) + { + _currentDragOperation.CancellationTokenSource?.Cancel(); + _currentDragOperation.Dispose(); + _currentDragOperation = null; + } + + ClearAllDropTargets(); + + _dropTargetsLock.Dispose(); + + // Очистка событий + _dragStarted = null; + _dragUpdated = null; + _dropTargetChanged = null; + _dragCompleted = null; + _dragCancelled = null; + _errorOccurred = null; + + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + #endregion +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DragCancelledEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DragCancelledEventArgs.cs new file mode 100644 index 0000000..495d904 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DragCancelledEventArgs.cs @@ -0,0 +1,22 @@ +using Lattice.Core.DragDrop.Models; + +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события отмены перетаскивания. +/// +public class DragCancelledEventArgs : EventArgs +{ + /// + /// Информация о перетаскивании. + /// + public DragInfo DragInfo { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragCancelledEventArgs(DragInfo dragInfo) + { + DragInfo = dragInfo; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DragCompletedEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DragCompletedEventArgs.cs new file mode 100644 index 0000000..bc1a747 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DragCompletedEventArgs.cs @@ -0,0 +1,35 @@ +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; + +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события завершения перетаскивания. +/// +public class DragCompletedEventArgs : EventArgs +{ + /// + /// Информация о перетаскивании. + /// + public DragInfo DragInfo { get; } + + /// + /// Позиция завершения перетаскивания. + /// + public Point DropPosition { get; } + + /// + /// Примененные эффекты перетаскивания. + /// + public Enums.DragDropEffects Effects { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragCompletedEventArgs(DragInfo dragInfo, Point dropPosition, Enums.DragDropEffects effects) + { + DragInfo = dragInfo; + DropPosition = dropPosition; + Effects = effects; + } +} diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DragDropErrorEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DragDropErrorEventArgs.cs new file mode 100644 index 0000000..4a7ce23 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DragDropErrorEventArgs.cs @@ -0,0 +1,32 @@ +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события ошибки в операции перетаскивания. +/// +public class DragDropErrorEventArgs : EventArgs +{ + /// + /// Ошибка, которая произошла. + /// + public Exception Exception { get; } + + /// + /// Операция, во время которой произошла ошибка. + /// + public string Operation { get; } + + /// + /// Контекст операции. + /// + public object? Context { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragDropErrorEventArgs(Exception exception, string operation, object? context = null) + { + Exception = exception; + Operation = operation; + Context = context; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DragStartedEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DragStartedEventArgs.cs new file mode 100644 index 0000000..85386b7 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DragStartedEventArgs.cs @@ -0,0 +1,29 @@ +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; + +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события начала перетаскивания. +/// +public class DragStartedEventArgs : EventArgs +{ + /// + /// Информация о перетаскивании. + /// + public DragInfo DragInfo { get; } + + /// + /// Начальная позиция перетаскивания. + /// + public Point StartPosition { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragStartedEventArgs(DragInfo dragInfo, Point startPosition) + { + DragInfo = dragInfo; + StartPosition = startPosition; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DragUpdatedEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DragUpdatedEventArgs.cs new file mode 100644 index 0000000..504c505 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DragUpdatedEventArgs.cs @@ -0,0 +1,29 @@ +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; + +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события обновления перетаскивания. +/// +public class DragUpdatedEventArgs : EventArgs +{ + /// + /// Информация о перетаскивании. + /// + public DragInfo DragInfo { get; } + + /// + /// Текущая позиция перетаскивания. + /// + public Point Position { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DragUpdatedEventArgs(DragInfo dragInfo, Point position) + { + DragInfo = dragInfo; + Position = position; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/EventArgs/DropTargetChangedEventArgs.cs b/Lattice.Core.DragDrop/Services/EventArgs/DropTargetChangedEventArgs.cs new file mode 100644 index 0000000..9003fe9 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/EventArgs/DropTargetChangedEventArgs.cs @@ -0,0 +1,35 @@ +using Lattice.Core.DragDrop.Models; +using Lattice.Core.Geometry; + +namespace Lattice.Core.DragDrop.Services; + +/// +/// Аргументы события изменения цели сброса. +/// +public class DropTargetChangedEventArgs : EventArgs +{ + /// + /// Информация о перетаскивании. + /// + public DragInfo DragInfo { get; } + + /// + /// Новая цель сброса. + /// + public Abstractions.IDropTarget Target { get; } + + /// + /// Границы цели. + /// + public Rect TargetBounds { get; } + + /// + /// Инициализирует новый экземпляр класса . + /// + public DropTargetChangedEventArgs(DragInfo dragInfo, Abstractions.IDropTarget target, Rect targetBounds) + { + DragInfo = dragInfo; + Target = target; + TargetBounds = targetBounds; + } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Services/IDragDropService.cs b/Lattice.Core.DragDrop/Services/IDragDropService.cs new file mode 100644 index 0000000..9d01977 --- /dev/null +++ b/Lattice.Core.DragDrop/Services/IDragDropService.cs @@ -0,0 +1,174 @@ +namespace Lattice.Core.DragDrop.Services; + +/// +/// Предоставляет централизованный сервис для управления операциями перетаскивания. +/// +public interface IDragDropService : IDisposable +{ + #region Свойства + + /// + /// Активна ли операция перетаскивания. + /// + bool IsDragActive { get; } + + /// + /// Информация о текущей операции. + /// + Models.DragInfo? CurrentDragInfo { get; } + + /// + /// Текущая цель сброса. + /// + Abstractions.IDropTarget? CurrentDropTarget { get; } + + /// + /// Порог начала перетаскивания в пикселях. + /// + double DragStartThreshold { get; set; } + + /// + /// Включены ли асинхронные операции. + /// + bool EnableAsyncOperations { get; set; } + + /// + /// Максимальное время ожидания асинхронной операции (мс). + /// + int AsyncOperationTimeout { get; set; } + + #endregion + + #region События + + /// + /// Событие начала операции перетаскивания. + /// + event EventHandler DragStarted; + + /// + /// Событие обновления позиции перетаскивания. + /// + event EventHandler DragUpdated; + + /// + /// Событие изменения цели сброса. + /// + event EventHandler DropTargetChanged; + + /// + /// Событие завершения операции перетаскивания. + /// + event EventHandler DragCompleted; + + /// + /// Событие отмены операции перетаскивания. + /// + event EventHandler DragCancelled; + + /// + /// Событие ошибки в операции перетаскивания. + /// + event EventHandler ErrorOccurred; + + #endregion + + #region Регистрация целей сброса + + /// + /// Регистрирует цель сброса. + /// + string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null); + + /// + /// Обновляет границы цели сброса. + /// + bool UpdateDropTargetBounds(string id, Geometry.Rect bounds); + + /// + /// Отменяет регистрацию цели сброса. + /// + bool UnregisterDropTarget(string id); + + /// + /// Отменяет регистрацию всех целей в группе. + /// + void UnregisterDropTargetsInGroup(string group); + + #endregion + + #region Асинхронные операции + + /// + /// Начинает операцию перетаскивания (асинхронно). + /// + Task StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition); + + /// + /// Обновляет позицию перетаскивания (асинхронно). + /// + Task UpdateDragAsync(Geometry.Point position); + + /// + /// Завершает операцию перетаскивания (асинхронно). + /// + Task EndDragAsync(Geometry.Point position); + + /// + /// Отменяет операцию перетаскивания (асинхронно). + /// + Task CancelDragAsync(); + + #endregion + + #region Синхронные операции (для обратной совместимости) + + /// + /// Начинает операцию перетаскивания (синхронно). + /// + bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition); + + /// + /// Обновляет позицию перетаскивания (синхронно). + /// + void UpdateDrag(Geometry.Point position); + + /// + /// Завершает операцию перетаскивания (синхронно). + /// + Enums.DragDropEffects EndDrag(Geometry.Point position); + + /// + /// Отменяет операцию перетаскивания (синхронно). + /// + void CancelDrag(); + + #endregion + + #region Утилиты + + /// + /// Очищает все зарегистрированные цели. + /// + void ClearAllDropTargets(); + + /// + /// Получает статистику использования. + /// + DragDropStats GetStats(); + + #endregion +} + +/// +/// Статистика использования Drag & Drop. +/// +public class DragDropStats +{ + public int TotalDragOperations { get; set; } + public int SuccessfulDrops { get; set; } + public int CancelledOperations { get; set; } + public int ErrorCount { get; set; } + public int RegisteredTargets { get; set; } + public TimeSpan AverageOperationTime { get; set; } +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs b/Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs new file mode 100644 index 0000000..a0f605f --- /dev/null +++ b/Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs @@ -0,0 +1,713 @@ +using Lattice.Core.DragDrop.Abstractions; +using Lattice.Core.DragDrop.Enums; +using Lattice.Core.DragDrop.Models; + +namespace Lattice.Core.DragDrop.Utilities; + +/// +/// Предоставляет утилитарные методы и фабричные методы для работы с системой перетаскивания с поддержкой async. +/// +public static class AsyncDragDropUtilities +{ + /// + /// Создает асинхронную реализацию источника перетаскивания. + /// + public static IAsyncDragSource CreateAsyncDragSource( + Func> dataProviderAsync, + Func>? canDragAsync = null, + Func? onCompletedAsync = null, + Func? onCancelledAsync = null) + { + return new AsyncDragSourceWrapper(dataProviderAsync, canDragAsync, onCompletedAsync, onCancelledAsync); + } + + /// + /// Создает асинхронную реализацию цели сброса. + /// + public static IAsyncDropTarget CreateAsyncDropTarget( + Func>? canAcceptAsync = null, + Func? onDragOverAsync = null, + Func? onDropAsync = null, + Func? onDragLeaveAsync = null) + { + return new AsyncDropTargetWrapper(canAcceptAsync, onDragOverAsync, onDropAsync, onDragLeaveAsync); + } + + /// + /// Создает адаптер для преобразования синхронного источника в асинхронный. + /// + public static IAsyncDragSource CreateAsyncAdapter(IDragSource syncSource) + { + return new SyncToAsyncDragSourceAdapter(syncSource); + } + + /// + /// Создает адаптер для преобразования синхронной цели в асинхронную. + /// + public static IAsyncDropTarget CreateAsyncAdapter(IDropTarget syncTarget) + { + return new SyncToAsyncDropTargetAdapter(syncTarget); + } + + #region Обертки-реализации + + /// + /// Обертка для создания асинхронного источника перетаскивания. + /// + private sealed class AsyncDragSourceWrapper : IAsyncDragSource + { + private readonly Func> _dataProviderAsync; + private readonly Func>? _canDragAsync; + private readonly Func? _onCompletedAsync; + private readonly Func? _onCancelledAsync; + + public AsyncDragSourceWrapper( + Func> dataProviderAsync, + Func>? canDragAsync = null, + Func? onCompletedAsync = null, + Func? onCancelledAsync = null) + { + _dataProviderAsync = dataProviderAsync ?? throw new ArgumentNullException(nameof(dataProviderAsync)); + _canDragAsync = canDragAsync; + _onCompletedAsync = onCompletedAsync; + _onCancelledAsync = onCancelledAsync; + } + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + try + { + // Проверяем, может ли начаться перетаскивание + if (_canDragAsync != null) + { + var canDrag = await _canDragAsync().ConfigureAwait(false); + if (!canDrag) + return (false, null); + } + + // Получаем данные + var data = await _dataProviderAsync().ConfigureAwait(false); + if (data == null) + return (false, null); + + // Создаем информацию о перетаскивании + var dragInfo = DragDropUtilities.CreateDragInfo( + data, + Geometry.Point.Zero, + DragDropEffects.Copy | DragDropEffects.Move, + this); + + return (true, dragInfo); + } + catch + { + return (false, null); + } + } + + public Task StartDragAsync(DragInfo dragInfo) + { + // Базовая реализация всегда разрешает начало + return Task.FromResult(true); + } + + public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects) + { + if (_onCompletedAsync != null) + { + await _onCompletedAsync(dragInfo, effects).ConfigureAwait(false); + } + } + + public async Task DragCancelledAsync(DragInfo dragInfo) + { + if (_onCancelledAsync != null) + { + await _onCancelledAsync(dragInfo).ConfigureAwait(false); + } + } + + #region Синхронная реализация (для IDragSource) + + public bool CanStartDrag(out DragInfo? dragInfo) + { + // Для синхронного вызова используем Task.Result + var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult(); + dragInfo = result.DragInfo; + return result.CanStart; + } + + public bool StartDrag(DragInfo dragInfo) + { + return Task.Run(() => StartDragAsync(dragInfo)).GetAwaiter().GetResult(); + } + + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + Task.Run(() => DragCompletedAsync(dragInfo, effects)).Wait(); + } + + public void DragCancelled(DragInfo dragInfo) + { + Task.Run(() => DragCancelledAsync(dragInfo)).Wait(); + } + + #endregion + } + + /// + /// Обертка для создания асинхронной цели сброса. + /// + private sealed class AsyncDropTargetWrapper : IAsyncDropTarget + { + private readonly Func>? _canAcceptAsync; + private readonly Func? _onDragOverAsync; + private readonly Func? _onDropAsync; + private readonly Func? _onDragLeaveAsync; + + public AsyncDropTargetWrapper( + Func>? canAcceptAsync = null, + Func? onDragOverAsync = null, + Func? onDropAsync = null, + Func? onDragLeaveAsync = null) + { + _canAcceptAsync = canAcceptAsync; + _onDragOverAsync = onDragOverAsync; + _onDropAsync = onDropAsync; + _onDragLeaveAsync = onDragLeaveAsync; + } + + public async Task CanAcceptDropAsync(DropInfo dropInfo) + { + try + { + if (_canAcceptAsync != null) + { + return await _canAcceptAsync(dropInfo).ConfigureAwait(false); + } + return true; // По умолчанию принимаем все + } + catch + { + return false; // При ошибке не принимаем + } + } + + public async Task DragOverAsync(DropInfo dropInfo) + { + try + { + if (_onDragOverAsync != null) + { + await _onDragOverAsync(dropInfo).ConfigureAwait(false); + } + } + catch + { + // Игнорируем ошибки в обработчике + } + } + + public async Task DropAsync(DropInfo dropInfo) + { + try + { + if (_onDropAsync != null) + { + await _onDropAsync(dropInfo).ConfigureAwait(false); + } + } + catch + { + // Игнорируем ошибки в обработчике + } + } + + public async Task DragLeaveAsync() + { + try + { + if (_onDragLeaveAsync != null) + { + await _onDragLeaveAsync().ConfigureAwait(false); + } + } + catch + { + // Игнорируем ошибки в обработчике + } + } + + #region Синхронная реализация (для IDropTarget) + + public bool CanAcceptDrop(DropInfo dropInfo) + { + return Task.Run(() => CanAcceptDropAsync(dropInfo)).GetAwaiter().GetResult(); + } + + public void DragOver(DropInfo dropInfo) + { + Task.Run(() => DragOverAsync(dropInfo)).Wait(); + } + + public void Drop(DropInfo dropInfo) + { + Task.Run(() => DropAsync(dropInfo)).Wait(); + } + + public void DragLeave() + { + Task.Run(DragLeaveAsync).Wait(); + } + + #endregion + } + + /// + /// Адаптер для преобразования синхронного источника в асинхронный. + /// + private sealed class SyncToAsyncDragSourceAdapter : IAsyncDragSource + { + private readonly IDragSource _syncSource; + + public SyncToAsyncDragSourceAdapter(IDragSource syncSource) + { + _syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource)); + } + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + return await Task.Run(() => + { + var canStart = _syncSource.CanStartDrag(out var dragInfo); + return (canStart, dragInfo); + }).ConfigureAwait(false); + } + + public async Task StartDragAsync(DragInfo dragInfo) + { + return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false); + } + + public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects) + { + await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false); + } + + public async Task DragCancelledAsync(DragInfo dragInfo) + { + await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false); + } + + #region Синхронная реализация (делегируем синхронному источнику) + + public bool CanStartDrag(out DragInfo? dragInfo) + { + return _syncSource.CanStartDrag(out dragInfo); + } + + public bool StartDrag(DragInfo dragInfo) + { + return _syncSource.StartDrag(dragInfo); + } + + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + _syncSource.DragCompleted(dragInfo, effects); + } + + public void DragCancelled(DragInfo dragInfo) + { + _syncSource.DragCancelled(dragInfo); + } + + #endregion + } + + /// + /// Адаптер для преобразования синхронной цели в асинхронную. + /// + private sealed class SyncToAsyncDropTargetAdapter : IAsyncDropTarget + { + private readonly IDropTarget _syncTarget; + + public SyncToAsyncDropTargetAdapter(IDropTarget syncTarget) + { + _syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget)); + } + + public async Task CanAcceptDropAsync(DropInfo dropInfo) + { + return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false); + } + + public async Task DragOverAsync(DropInfo dropInfo) + { + await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false); + } + + public async Task DropAsync(DropInfo dropInfo) + { + await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false); + } + + public async Task DragLeaveAsync() + { + await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false); + } + + #region Синхронная реализация (делегируем синхронной цели) + + public bool CanAcceptDrop(DropInfo dropInfo) + { + return _syncTarget.CanAcceptDrop(dropInfo); + } + + public void DragOver(DropInfo dropInfo) + { + _syncTarget.DragOver(dropInfo); + } + + public void Drop(DropInfo dropInfo) + { + _syncTarget.Drop(dropInfo); + } + + public void DragLeave() + { + _syncTarget.DragLeave(); + } + + #endregion + } + + #endregion + + #region Утилитарные методы + + /// + /// Выполняет асинхронную операцию с таймаутом. + /// + public static async Task ExecuteWithTimeoutAsync( + Task task, + TimeSpan timeout, + T defaultValue = default!) + { + if (timeout <= TimeSpan.Zero) + return await task.ConfigureAwait(false); + + var delayTask = Task.Delay(timeout); + var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false); + + if (completedTask == delayTask) + { + return defaultValue; + } + + return await task.ConfigureAwait(false); + } + + /// + /// Выполняет асинхронную операцию с таймаутом. + /// + public static async Task ExecuteWithTimeoutAsync( + Task task, + TimeSpan timeout) + { + if (timeout <= TimeSpan.Zero) + { + await task.ConfigureAwait(false); + return true; + } + + var delayTask = Task.Delay(timeout); + var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false); + + return completedTask == task; + } + + /// + /// Выполняет асинхронную операцию с таймаутом и обработкой ошибок. + /// + public static async Task ExecuteSafeWithTimeoutAsync( + Task task, + TimeSpan timeout, + Func errorHandler = null) where T : class + { + try + { + if (timeout <= TimeSpan.Zero) + return await task.ConfigureAwait(false); + + var delayTask = Task.Delay(timeout); + var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false); + + if (completedTask == delayTask) + { + return default; + } + + return await task.ConfigureAwait(false); + } + catch (Exception ex) + { + return errorHandler?.Invoke(ex) ?? default; + } + } + + /// + /// Создает комбинированный источник из синхронного и асинхронного. + /// + public static IAsyncDragSource Combine( + IDragSource syncSource, + IAsyncDragSource asyncSource, + bool preferAsync = true) + { + return new CombinedDragSource(syncSource, asyncSource, preferAsync); + } + + /// + /// Создает комбинированную цель из синхронной и асинхронной. + /// + public static IAsyncDropTarget Combine( + IDropTarget syncTarget, + IAsyncDropTarget asyncTarget, + bool preferAsync = true) + { + return new CombinedDropTarget(syncTarget, asyncTarget, preferAsync); + } + + #endregion + + #region Комбинированные реализации + + /// + /// Комбинированный источник, поддерживающий как синхронный, так и асинхронный API. + /// + private sealed class CombinedDragSource : IAsyncDragSource + { + private readonly IDragSource _syncSource; + private readonly IAsyncDragSource _asyncSource; + private readonly bool _preferAsync; + + public CombinedDragSource(IDragSource syncSource, IAsyncDragSource asyncSource, bool preferAsync) + { + _syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource)); + _asyncSource = asyncSource ?? throw new ArgumentNullException(nameof(asyncSource)); + _preferAsync = preferAsync; + } + + public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() + { + if (_preferAsync) + { + try + { + return await _asyncSource.CanStartDragAsync().ConfigureAwait(false); + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + // Используем синхронную версию в отдельной задаче + return await Task.Run(() => + { + var canStart = _syncSource.CanStartDrag(out var dragInfo); + return (canStart, dragInfo); + }).ConfigureAwait(false); + } + + public async Task StartDragAsync(DragInfo dragInfo) + { + if (_preferAsync) + { + try + { + return await _asyncSource.StartDragAsync(dragInfo).ConfigureAwait(false); + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false); + } + + public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects) + { + if (_preferAsync) + { + try + { + await _asyncSource.DragCompletedAsync(dragInfo, effects).ConfigureAwait(false); + return; + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false); + } + + public async Task DragCancelledAsync(DragInfo dragInfo) + { + if (_preferAsync) + { + try + { + await _asyncSource.DragCancelledAsync(dragInfo).ConfigureAwait(false); + return; + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false); + } + + #region Синхронная реализация + + public bool CanStartDrag(out DragInfo? dragInfo) + { + return _syncSource.CanStartDrag(out dragInfo); + } + + public bool StartDrag(DragInfo dragInfo) + { + return _syncSource.StartDrag(dragInfo); + } + + public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) + { + _syncSource.DragCompleted(dragInfo, effects); + } + + public void DragCancelled(DragInfo dragInfo) + { + _syncSource.DragCancelled(dragInfo); + } + + #endregion + } + + /// + /// Комбинированная цель, поддерживающая как синхронный, так и асинхронный API. + /// + private sealed class CombinedDropTarget : IAsyncDropTarget + { + private readonly IDropTarget _syncTarget; + private readonly IAsyncDropTarget _asyncTarget; + private readonly bool _preferAsync; + + public CombinedDropTarget(IDropTarget syncTarget, IAsyncDropTarget asyncTarget, bool preferAsync) + { + _syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget)); + _asyncTarget = asyncTarget ?? throw new ArgumentNullException(nameof(asyncTarget)); + _preferAsync = preferAsync; + } + + public async Task CanAcceptDropAsync(DropInfo dropInfo) + { + if (_preferAsync) + { + try + { + return await _asyncTarget.CanAcceptDropAsync(dropInfo).ConfigureAwait(false); + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false); + } + + public async Task DragOverAsync(DropInfo dropInfo) + { + if (_preferAsync) + { + try + { + await _asyncTarget.DragOverAsync(dropInfo).ConfigureAwait(false); + return; + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false); + } + + public async Task DropAsync(DropInfo dropInfo) + { + if (_preferAsync) + { + try + { + await _asyncTarget.DropAsync(dropInfo).ConfigureAwait(false); + return; + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false); + } + + public async Task DragLeaveAsync() + { + if (_preferAsync) + { + try + { + await _asyncTarget.DragLeaveAsync().ConfigureAwait(false); + return; + } + catch + { + // В случае ошибки пробуем синхронную версию + } + } + + await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false); + } + + #region Синхронная реализация + + public bool CanAcceptDrop(DropInfo dropInfo) + { + return _syncTarget.CanAcceptDrop(dropInfo); + } + + public void DragOver(DropInfo dropInfo) + { + _syncTarget.DragOver(dropInfo); + } + + public void Drop(DropInfo dropInfo) + { + _syncTarget.Drop(dropInfo); + } + + public void DragLeave() + { + _syncTarget.DragLeave(); + } + + #endregion + } + + #endregion +} \ No newline at end of file diff --git a/Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs b/Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs new file mode 100644 index 0000000..c1486f7 --- /dev/null +++ b/Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs @@ -0,0 +1,275 @@ +namespace Lattice.Core.DragDrop.Utilities; + +/// +/// Утилиты для работы с системой перетаскивания. +/// +public static class DragDropUtilities +{ + #region Effect Utilities + + /// + /// Проверяет, совместимы ли эффекты источника и цели. + /// + public static bool AreEffectsCompatible(Enums.DragDropEffects sourceEffects, Enums.DragDropEffects targetEffects) + { + if (sourceEffects == Enums.DragDropEffects.None || targetEffects == Enums.DragDropEffects.None) + return false; + + return (sourceEffects & targetEffects) != Enums.DragDropEffects.None; + } + + /// + /// Получает наиболее подходящий эффект на основе доступных. + /// + public static Enums.DragDropEffects GetBestEffect(Enums.DragDropEffects available, Enums.DragDropEffects preferred) + { + if ((available & preferred) != Enums.DragDropEffects.None) + return available & preferred; + + if ((available & Enums.DragDropEffects.Move) != Enums.DragDropEffects.None) + return Enums.DragDropEffects.Move; + + if ((available & Enums.DragDropEffects.Copy) != Enums.DragDropEffects.None) + return Enums.DragDropEffects.Copy; + + if ((available & Enums.DragDropEffects.Link) != Enums.DragDropEffects.None) + return Enums.DragDropEffects.Link; + + return Enums.DragDropEffects.None; + } + + #endregion + + #region Geometry Utilities + + /// + /// Вычисляет расстояние между двумя точками. + /// + public static double CalculateDistance(Geometry.Point p1, Geometry.Point p2) + { + var dx = p2.X - p1.X; + var dy = p2.Y - p1.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + /// + /// Проверяет, превысило ли перемещение пороговое значение. + /// + public static bool HasExceededDragThreshold(Geometry.Point startPoint, Geometry.Point currentPoint, double threshold) + { + var distance = CalculateDistance(startPoint, currentPoint); + return distance >= threshold; + } + + /// + /// Определяет позицию сброса относительно прямоугольника. + /// + public static Enums.DropPosition GetDropPosition(Geometry.Point point, Geometry.Rect bounds, double edgeThreshold = 20.0) + { + if (!bounds.Contains(new Geometry.Point(point.X, point.Y))) + return Enums.DropPosition.Inside; + + var relativeX = (point.X - bounds.X) / bounds.Width; + var relativeY = (point.Y - bounds.Y) / bounds.Height; + + if (relativeX < edgeThreshold / bounds.Width) + return Enums.DropPosition.Left; + if (relativeX > 1 - edgeThreshold / bounds.Width) + return Enums.DropPosition.Right; + if (relativeY < edgeThreshold / bounds.Height) + return Enums.DropPosition.Top; + if (relativeY > 1 - edgeThreshold / bounds.Height) + return Enums.DropPosition.Bottom; + + return Enums.DropPosition.Center; + } + + #endregion + + #region Factory Methods + + /// + /// Создает информацию о перетаскивании. + /// + public static Models.DragInfo CreateDragInfo( + object data, + Geometry.Point startPosition, + Enums.DragDropEffects allowedEffects = Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move, + object? source = null, + Dictionary? parameters = null) + { + var dragInfo = new Models.DragInfo(data, allowedEffects, startPosition, source); + + if (parameters != null) + { + foreach (var param in parameters) + { + dragInfo.SetParameter(param.Key, param.Value); + } + } + + return dragInfo; + } + + /// + /// Создает простую реализацию источника перетаскивания. + /// + public static Abstractions.IDragSource CreateSimpleDragSource( + Func dataProvider, + Func? canDrag = null, + Action? onCompleted = null, + Action? onCancelled = null) + { + return new SimpleDragSource(dataProvider, canDrag, onCompleted, onCancelled); + } + + /// + /// Создает простую реализацию цели сброса. + /// + public static Abstractions.IDropTarget CreateSimpleDropTarget( + Func? canAccept = null, + Action? onDragOver = null, + Action? onDrop = null, + Action? onDragLeave = null) + { + return new SimpleDropTarget(canAccept, onDragOver, onDrop, onDragLeave); + } + + #endregion + + #region Data Extraction + + /// + /// Извлекает данные из элемента с поддержкой различных паттернов. + /// + public static object? ExtractData(object? element) + { + if (element == null) + return null; + + // Проверяем, реализует ли элемент специальный интерфейс + if (element is Abstractions.IDragSource dragSource) + { + if (dragSource.CanStartDrag(out var dragInfo) && dragInfo != null) + return dragInfo.Data; + } + + // В реальной реализации здесь будет рефлексия для проверки свойств + // DataContext, Content и т.д. + + // Возвращаем сам элемент как данные + return element; + } + + /// + /// Проверяет, совместимы ли данные с указанными типами. + /// + public static bool IsDataCompatible(object? data, IEnumerable? acceptedTypes) + { + if (data == null || acceptedTypes == null) + return false; + + var dataType = data.GetType(); + + foreach (var acceptedType in acceptedTypes) + { + if (acceptedType.IsAssignableFrom(dataType)) + return true; + } + + return false; + } + + #endregion + + #region Helper Classes + + private sealed class SimpleDragSource : Abstractions.IDragSource + { + private readonly Func _dataProvider; + private readonly Func? _canDrag; + private readonly Action? _onCompleted; + private readonly Action? _onCancelled; + + public SimpleDragSource( + Func dataProvider, + Func? canDrag = null, + Action? onCompleted = null, + Action? onCancelled = null) + { + _dataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + _canDrag = canDrag; + _onCompleted = onCompleted; + _onCancelled = onCancelled; + } + + public bool CanStartDrag(out Models.DragInfo? dragInfo) + { + dragInfo = null; + + if (_canDrag?.Invoke() == false) + return false; + + var data = _dataProvider(); + if (data == null) + return false; + + dragInfo = CreateDragInfo(data, Geometry.Point.Zero, Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move, this); + return true; + } + + public bool StartDrag(Models.DragInfo dragInfo) => true; + + public void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects) + { + _onCompleted?.Invoke(dragInfo, effects); + } + + public void DragCancelled(Models.DragInfo dragInfo) + { + _onCancelled?.Invoke(dragInfo); + } + } + + private sealed class SimpleDropTarget : Abstractions.IDropTarget + { + private readonly Func? _canAccept; + private readonly Action? _onDragOver; + private readonly Action? _onDrop; + private readonly Action? _onDragLeave; + + public SimpleDropTarget( + Func? canAccept = null, + Action? onDragOver = null, + Action? onDrop = null, + Action? onDragLeave = null) + { + _canAccept = canAccept; + _onDragOver = onDragOver; + _onDrop = onDrop; + _onDragLeave = onDragLeave; + } + + public bool CanAcceptDrop(Models.DropInfo dropInfo) + { + return _canAccept?.Invoke(dropInfo) ?? true; + } + + public void DragOver(Models.DropInfo dropInfo) + { + _onDragOver?.Invoke(dropInfo); + } + + public void Drop(Models.DropInfo dropInfo) + { + _onDrop?.Invoke(dropInfo); + } + + public void DragLeave() + { + _onDragLeave?.Invoke(); + } + } + + #endregion +} \ No newline at end of file diff --git a/Lattice.Core.Geometry/Lattice.Core.Geometry.csproj b/Lattice.Core.Geometry/Lattice.Core.Geometry.csproj new file mode 100644 index 0000000..6e5b253 --- /dev/null +++ b/Lattice.Core.Geometry/Lattice.Core.Geometry.csproj @@ -0,0 +1,9 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + + + diff --git a/Lattice.Core.Geometry/Point.cs b/Lattice.Core.Geometry/Point.cs new file mode 100644 index 0000000..10e66a3 --- /dev/null +++ b/Lattice.Core.Geometry/Point.cs @@ -0,0 +1,79 @@ +namespace Lattice.Core.Geometry; + +/// +/// Представляет точку в двумерном пространстве с координатами X и Y. +/// Эта структура является платформонезависимой и может использоваться +/// во всех слоях системы Lattice. +/// +public struct Point : IEquatable +{ + /// + /// Получает точку с координатами (0, 0). + /// + public static readonly Point Zero = new(0, 0); + + /// + /// Координата X (горизонтальная). + /// + public double X { get; set; } + + /// + /// Координата Y (вертикальная). + /// + public double Y { get; set; } + + /// + /// Инициализирует новую точку с указанными координатами. + /// + /// Координата X. + /// Координата Y. + public Point(double x, double y) + { + X = x; + Y = y; + } + + /// + /// Создает точку из System.Drawing.Point. + /// + public static Point FromDrawingPoint(System.Drawing.Point point) => + new(point.X, point.Y); + + /// + /// Преобразует точку в System.Drawing.Point. + /// + public System.Drawing.Point ToDrawingPoint() => + new((int)X, (int)Y); + + /// + /// Определяет, равна ли эта точка другой точке. + /// + public bool Equals(Point other) => + Math.Abs(X - other.X) < double.Epsilon && + Math.Abs(Y - other.Y) < double.Epsilon; + + /// + public override bool Equals(object? obj) => + obj is Point point && Equals(point); + + /// + public override int GetHashCode() => + HashCode.Combine(X, Y); + + /// + /// Определяет, равны ли две точки. + /// + public static bool operator ==(Point left, Point right) => + left.Equals(right); + + /// + /// Определяет, не равны ли две точки. + /// + public static bool operator !=(Point left, Point right) => + !left.Equals(right); + + /// + /// Возвращает строковое представление точки. + /// + public override string ToString() => $"{X}, {Y}"; +} diff --git a/Lattice.Core.Geometry/Rect.cs b/Lattice.Core.Geometry/Rect.cs new file mode 100644 index 0000000..07d7c13 --- /dev/null +++ b/Lattice.Core.Geometry/Rect.cs @@ -0,0 +1,153 @@ +namespace Lattice.Core.Geometry; + +/// +/// Представляет прямоугольник в двумерном пространстве с позицией и размерами. +/// Эта структура является платформонезависимой и может использоваться +/// во всех слоях системы Lattice. +/// +public struct Rect : IEquatable +{ + /// + /// Получает пустой прямоугольник (позиция (0, 0), размеры (0, 0)). + /// + public static readonly Rect Empty = new(0, 0, 0, 0); + + /// + /// Координата X левого верхнего угла прямоугольника. + /// + public double X { get; set; } + + /// + /// Координата Y левого верхнего угла прямоугольника. + /// + public double Y { get; set; } + + /// + /// Ширина прямоугольника. + /// + public double Width { get; set; } + + /// + /// Высота прямоугольника. + /// + public double Height { get; set; } + + /// + /// Получает координату X правого края прямоугольника. + /// + public double Right => X + Width; + + /// + /// Получает координату Y нижнего края прямоугольника. + /// + public double Bottom => Y + Height; + + /// + /// Получает левый верхний угол прямоугольника. + /// + public Point TopLeft => new(X, Y); + + /// + /// Получает правый нижний угол прямоугольника. + /// + public Point BottomRight => new(Right, Bottom); + + /// + /// Получает центр прямоугольника. + /// + public Point Center => new(X + Width / 2, Y + Height / 2); + + /// + /// Получает площадь прямоугольника. + /// + public double Area => Width * Height; + + /// + /// Инициализирует новый прямоугольник с указанными параметрами. + /// + /// Координата X. + /// Координата Y. + /// Ширина. + /// Высота. + public Rect(double x, double y, double width, double height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + /// + /// Инициализирует новый прямоугольник с указанной позицией и размером. + /// + /// Позиция прямоугольника. + /// Размер прямоугольника. + public Rect(Point location, Size size) + { + X = location.X; + Y = location.Y; + Width = size.Width; + Height = size.Height; + } + + /// + /// Создает прямоугольник из System.Drawing.Rectangle. + /// + public static Rect FromDrawingRectangle(System.Drawing.Rectangle rect) => + new(rect.X, rect.Y, rect.Width, rect.Height); + + /// + /// Преобразует прямоугольник в System.Drawing.Rectangle. + /// + public System.Drawing.Rectangle ToDrawingRectangle() => + new((int)X, (int)Y, (int)Width, (int)Height); + + /// + /// Проверяет, содержит ли прямоугольник указанную точку. + /// + public bool Contains(Point point) => + point.X >= X && point.X <= Right && + point.Y >= Y && point.Y <= Bottom; + + /// + /// Проверяет, пересекается ли этот прямоугольник с другим. + /// + public bool Intersects(Rect other) => + X < other.Right && Right > other.X && + Y < other.Bottom && Bottom > other.Y; + + /// + /// Определяет, равен ли этот прямоугольник другому прямоугольнику. + /// + public bool Equals(Rect other) => + Math.Abs(X - other.X) < double.Epsilon && + Math.Abs(Y - other.Y) < double.Epsilon && + Math.Abs(Width - other.Width) < double.Epsilon && + Math.Abs(Height - other.Height) < double.Epsilon; + + /// + public override bool Equals(object? obj) => + obj is Rect rect && Equals(rect); + + /// + public override int GetHashCode() => + HashCode.Combine(X, Y, Width, Height); + + /// + /// Определяет, равны ли два прямоугольника. + /// + public static bool operator ==(Rect left, Rect right) => + left.Equals(right); + + /// + /// Определяет, не равны ли два прямоугольника. + /// + public static bool operator !=(Rect left, Rect right) => + !left.Equals(right); + + /// + /// Возвращает строковое представление прямоугольника. + /// + public override string ToString() => + $"[X={X:F2}, Y={Y:F2}, Width={Width:F2}, Height={Height:F2}]"; +} \ No newline at end of file diff --git a/Lattice.Core.Geometry/Size.cs b/Lattice.Core.Geometry/Size.cs new file mode 100644 index 0000000..53db09b --- /dev/null +++ b/Lattice.Core.Geometry/Size.cs @@ -0,0 +1,84 @@ +namespace Lattice.Core.Geometry; + +/// +/// Представляет размеры в двумерном пространстве с шириной и высотой. +/// Эта структура является платформонезависимой и может использоваться +/// во всех слоях системы Lattice. +/// +public struct Size : IEquatable +{ + /// + /// Получает размер с нулевой шириной и высотой. + /// + public static readonly Size Zero = new(0, 0); + + /// + /// Ширина. + /// + public double Width { get; set; } + + /// + /// Высота. + /// + public double Height { get; set; } + + /// + /// Получает признак того, что размер является пустым (нулевая ширина или высота). + /// + public bool IsEmpty => Width <= 0 || Height <= 0; + + /// + /// Инициализирует новый размер с указанными значениями. + /// + /// Ширина. + /// Высота. + public Size(double width, double height) + { + Width = width; + Height = height; + } + + /// + /// Создает размер из System.Drawing.Size. + /// + public static Size FromDrawingSize(System.Drawing.Size size) => + new(size.Width, size.Height); + + /// + /// Преобразует размер в System.Drawing.Size. + /// + public System.Drawing.Size ToDrawingSize() => + new((int)Width, (int)Height); + + /// + /// Определяет, равен ли этот размер другому размеру. + /// + public bool Equals(Size other) => + Math.Abs(Width - other.Width) < double.Epsilon && + Math.Abs(Height - other.Height) < double.Epsilon; + + /// + public override bool Equals(object? obj) => + obj is Size size && Equals(size); + + /// + public override int GetHashCode() => + HashCode.Combine(Width, Height); + + /// + /// Определяет, равны ли два размера. + /// + public static bool operator ==(Size left, Size right) => + left.Equals(right); + + /// + /// Определяет, не равны ли два размера. + /// + public static bool operator !=(Size left, Size right) => + !left.Equals(right); + + /// + /// Возвращает строковое представление размера. + /// + public override string ToString() => $"{Width} × {Height}"; +} \ No newline at end of file diff --git a/Lattice.Core/Abstractions/IContextService.cs b/Lattice.Core/Abstractions/IContextService.cs deleted file mode 100644 index 67537ab..0000000 --- a/Lattice.Core/Abstractions/IContextService.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Lattice.Core.Abstractions; - -/// -/// Сервис управления контекстом приложения и связанными командами. -/// -public interface IContextService -{ - /// - /// Имя текущего активного контекста. - /// - string CurrentContext { get; } - - /// - /// Возникает при смене фокуса между вкладками с разными ContextGroup. - /// - event EventHandler? ContextChanged; - - /// - /// Устанавливает активный контекст. Вызывается UI-слоем при активации вкладки. - /// - void SetContext(string contextGroup); - - /// - /// Проверяет, должна ли команда быть видимой в текущем контексте. - /// - bool IsCommandVisible(string commandId, string commandContext); -} diff --git a/Lattice.Core/Abstractions/IDockableComponent.cs b/Lattice.Core/Abstractions/IDockableComponent.cs deleted file mode 100644 index 3aac000..0000000 --- a/Lattice.Core/Abstractions/IDockableComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Lattice.Core.Abstractions; - -/// -/// Описывает компонент, который может быть размещен внутри узла компоновки Lattice. -/// -public interface IDockableComponent -{ - /// - /// Уникальный строковый идентификатор компонента (например, "SolutionExplorer"). - /// - string UniqueId { get; } - - /// - /// Заголовок, отображаемый на вкладке или в заголовке панели. - /// - string DisplayName { get; } - - /// - /// Ключ иконки (для Segoe Fluent Icons или путей к ресурсам). - /// - string? IconKey { get; } - - /// - /// Группа контекста (например, "CodeEditor", "Debugger"). - /// Определяет, какие панели инструментов будут активны. - /// - string ContextGroup { get; } - - /// - /// Указывает, разрешено ли закрывать данный компонент пользователем. - /// - bool CanClose { get; } -} diff --git a/Lattice.Core/Abstractions/ILayoutElement.cs b/Lattice.Core/Abstractions/ILayoutElement.cs deleted file mode 100644 index bf70297..0000000 --- a/Lattice.Core/Abstractions/ILayoutElement.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace Lattice.Core.Abstractions; - -/// -/// Представляет базовый элемент иерархии компоновки Lattice. -/// -public interface ILayoutElement -{ - /// - /// Уникальный идентификатор элемента. - /// - Guid Id { get; } - - /// - /// Имя элемента для отображения или идентификации в логах. - /// - string Name { get; set; } - - /// - /// Значение ширины (в пикселях или долях "star"). - /// - double WidthValue { get; set; } - - /// - /// Указывает, является ли ширина пропорциональной (star). - /// - bool IsWidthStar { get; set; } - - /// - /// Значение высоты (в пикселях или долях "star"). - /// - double HeightValue { get; set; } - - /// - /// Указывает, является ли высота пропорциональной (star). - /// - bool IsHeightStar { get; set; } - - /// - /// Родительский элемент в дереве компоновки. - /// - ILayoutElement? Parent { get; set; } -} diff --git a/Lattice.Core/Abstractions/ILayoutService.cs b/Lattice.Core/Abstractions/ILayoutService.cs deleted file mode 100644 index 1653dee..0000000 --- a/Lattice.Core/Abstractions/ILayoutService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Lattice.Core.Models; -using Lattice.Core.Models.Enums; - -namespace Lattice.Core.Abstractions; - -/// -/// Сервис управления жизненным циклом макета приложения. -/// -public interface ILayoutService -{ - /// - /// Текущий корневой узел всей структуры окон. - /// - LayoutNode? Root { get; } - - /// - /// Событие, возникающее при любом изменении структуры (докинг, закрытие, изменение размеров). - /// - event EventHandler? LayoutUpdated; - - /// - /// Перемещает узел в указанную позицию относительно целевого узла. - /// - void Dock(LayoutNode source, LayoutNode target, DockDirection direction); - - /// - /// Удаляет узел из макета (например, при закрытии вкладки). - /// - void Remove(LayoutNode node); - - /// - /// Импортирует структуру макета из снапшота. - /// - void LoadLayout(string jsonData); - - /// - /// Экспортирует текущую структуру в строку для сохранения. - /// - string SaveLayout(); -} diff --git a/Lattice.Core/Abstractions/INotificationService.cs b/Lattice.Core/Abstractions/INotificationService.cs deleted file mode 100644 index 43e779d..0000000 --- a/Lattice.Core/Abstractions/INotificationService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Lattice.Core.Models; -using Lattice.Core.Models.Enums; - -namespace Lattice.Core.Abstractions; - -/// -/// Описывает сервис для рассылки уведомлений внутри системы Lattice. -/// -public interface INotificationService -{ - /// - /// Событие, возникающее при отправке нового сообщения. - /// - event EventHandler NotificationReceived; - - void Show(string message, NotificationSeverity severity = NotificationSeverity.Info, int durationSeconds = 5); -} \ No newline at end of file diff --git a/Lattice.Core/Lattice.Core.csproj b/Lattice.Core/Lattice.Core.csproj deleted file mode 100644 index 9fda911..0000000 --- a/Lattice.Core/Lattice.Core.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net8.0;net9.0;net10.0 - enable - enable - Lattice.Core - Lattice.Core - - FrigaT - FrigaT - https://git.frigat.duckdns.org/FrigaT/Lattice - https://git.frigat.duckdns.org/FrigaT/Lattice - Core docking and layout engine for Lattice UI (WinUI 3 / Uno Platform). - - true - latest - - - - - - - - diff --git a/Lattice.Core/Models/ActionDefinition.cs b/Lattice.Core/Models/ActionDefinition.cs deleted file mode 100644 index 35d7652..0000000 --- a/Lattice.Core/Models/ActionDefinition.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Lattice.Core.Models; - -/// -/// Определение действия (команды), которое может быть отображено в интерфейсе. -/// -public record ActionDefinition -{ - /// - /// Уникальный идентификатор команды. - /// - public string Id { get; init; } = Guid.NewGuid().ToString(); - - /// - /// Текст кнопки, отображаемый пользователю. - /// - public string Label { get; init; } = "Action"; - - /// - /// Код иконки из шрифта Segoe Fluent Icons (например, "\uE102"). - /// - public string IconKey { get; init; } = "\uE102"; - - /// - /// Группа контекста, к которой привязана кнопка (например, "CodeEditor", "Common"). - /// - public string TargetContext { get; init; } = "Common"; - - /// - /// Указывает, активна ли кнопка в данный момент. - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Подсказка, отображаемая при наведении (Tooltip). - /// - public string Tooltip { get; init; } = string.Empty; -} diff --git a/Lattice.Core/Models/ContentNode.cs b/Lattice.Core/Models/ContentNode.cs deleted file mode 100644 index 441edba..0000000 --- a/Lattice.Core/Models/ContentNode.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Lattice.Core.Abstractions; - -namespace Lattice.Core.Models; - -/// -/// Узел, представляющий конечный контент (вкладку, панель инструментов или документ). -/// -public class ContentNode : LayoutNode -{ - /// - /// Ссылка на визуальный или логический компонент, закрепленный в этом узле. - /// - public IDockableComponent? Component { get; set; } - - /// - /// Указывает, является ли данный узел частью основной рабочей области документов. - /// - public bool IsDocumentArea { get; set; } - - /// - /// Инициализирует новый экземпляр на основе компонента. - /// - /// Компонент содержимого. - public ContentNode(IDockableComponent component) - { - Component = component; - Name = component.DisplayName; - } -} diff --git a/Lattice.Core/Models/Enums/DockDirection.cs b/Lattice.Core/Models/Enums/DockDirection.cs deleted file mode 100644 index 76c725c..0000000 --- a/Lattice.Core/Models/Enums/DockDirection.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Lattice.Core.Models.Enums; - -public enum DockDirection -{ - Center, - Left, - Right, - Top, - Bottom, - Floating, -} diff --git a/Lattice.Core/Models/Enums/NotificationSeverity.cs b/Lattice.Core/Models/Enums/NotificationSeverity.cs deleted file mode 100644 index 4917f7b..0000000 --- a/Lattice.Core/Models/Enums/NotificationSeverity.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Lattice.Core.Models.Enums; - -public enum NotificationSeverity { - Info, - Success, - Warning, - Error, -} \ No newline at end of file diff --git a/Lattice.Core/Models/Enums/SplitOrientation.cs b/Lattice.Core/Models/Enums/SplitOrientation.cs deleted file mode 100644 index 42d3f67..0000000 --- a/Lattice.Core/Models/Enums/SplitOrientation.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Lattice.Core.Models.Enums; - -public enum SplitOrientation -{ - /// - /// Элементы располагаются друг за другом по горизонтали - /// - Horizontal, - /// - /// Элементы располагаются друг за другом по вертикали - /// - Vertical, -} \ No newline at end of file diff --git a/Lattice.Core/Models/LayoutNode.cs b/Lattice.Core/Models/LayoutNode.cs deleted file mode 100644 index 8da7743..0000000 --- a/Lattice.Core/Models/LayoutNode.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Lattice.Core.Abstractions; - -namespace Lattice.Core.Models; - -/// -/// Абстрактный базовый класс для всех узлов дерева компоновки. -/// -public abstract class LayoutNode : ILayoutElement -{ - /// - public Guid Id { get; } = Guid.NewGuid(); - - /// - public string Name { get; set; } = string.Empty; - - /// - public double WidthValue { get; set; } = 1.0; - - /// - public bool IsWidthStar { get; set; } = true; - - /// - public double HeightValue { get; set; } = 1.0; - - /// - public bool IsHeightStar { get; set; } = true; - - /// - public ILayoutElement? Parent { get; set; } - - /// - /// Возвращает строковое представление узла для отладки. - /// - public override string ToString() => $"{GetType().Name} [{Name}] ({Id.ToString()[..4]})"; -} diff --git a/Lattice.Core/Models/NotificationEventArgs.cs b/Lattice.Core/Models/NotificationEventArgs.cs deleted file mode 100644 index 45bbb64..0000000 --- a/Lattice.Core/Models/NotificationEventArgs.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Lattice.Core.Models.Enums; - -namespace Lattice.Core.Models; - -public record NotificationEventArgs(string Message, NotificationSeverity Severity, int DurationSeconds); \ No newline at end of file diff --git a/Lattice.Core/Models/SplitContainerNode.cs b/Lattice.Core/Models/SplitContainerNode.cs deleted file mode 100644 index 64e0d6c..0000000 --- a/Lattice.Core/Models/SplitContainerNode.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Lattice.Core.Models.Enums; - -namespace Lattice.Core.Models; - -/// -/// Узел-контейнер, разделяющий пространство между дочерними элементами в определенной ориентации. -/// -public class SplitContainerNode : LayoutNode -{ - /// - /// Ориентация разделения (горизонтальная или вертикальная). - /// - public SplitOrientation Orientation { get; set; } - - /// - /// Список дочерних узлов, находящихся внутри данного контейнера. - /// - public List Children { get; } = new(); - - /// - /// Инициализирует новый экземпляр . - /// - /// Ориентация контейнера. - public SplitContainerNode(SplitOrientation orientation) - { - Orientation = orientation; - } - - /// - /// Добавляет дочерний узел в контейнер и устанавливает связь с родителем. - /// - /// Узел для добавления. - public void AddChild(LayoutNode child) - { - child.Parent = this; - Children.Add(child); - } -} diff --git a/Lattice.Core/Models/WorkspaceSnapshot.cs b/Lattice.Core/Models/WorkspaceSnapshot.cs deleted file mode 100644 index ac7bef4..0000000 --- a/Lattice.Core/Models/WorkspaceSnapshot.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Lattice.Core.Models -{ - internal class WorkspaceSnapshot - { - } -} diff --git a/Lattice.Core/Persistence/LayoutJsonConverter.cs b/Lattice.Core/Persistence/LayoutJsonConverter.cs deleted file mode 100644 index 5b50f0a..0000000 --- a/Lattice.Core/Persistence/LayoutJsonConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Lattice.Core.Models; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Lattice.Core.Persistence; - -/// -/// Конвертер для полиморфной сериализации и десериализации узлов дерева Lattice. -/// -public class LayoutJsonConverter : JsonConverter -{ - public override LayoutNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var jsonDoc = JsonDocument.ParseValue(ref reader); - var rootElement = jsonDoc.RootElement; - - // Определяем тип узла по наличию специфических свойств - if (rootElement.TryGetProperty("Orientation", out _)) - { - return JsonSerializer.Deserialize(rootElement.GetRawText(), options); - } - - return JsonSerializer.Deserialize(rootElement.GetRawText(), options); - } - - public override void Write(Utf8JsonWriter writer, LayoutNode value, JsonSerializerOptions options) - { - // Используем стандартную сериализацию для конкретных типов - JsonSerializer.Serialize(writer, (object)value, value.GetType(), options); - } -} diff --git a/Lattice.Core/README.md b/Lattice.Core/README.md deleted file mode 100644 index 65fe142..0000000 --- a/Lattice.Core/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Lattice.Core - -[![Framework](img.shields.io)](#) -[![Author](img.shields.io)](git.frigat.duckdns.org) -[![Platform](img.shields.io)](#) - -**Lattice.Core** — это платформонезависимое ядро (Layout Engine) для построения сложных интерфейсов с системой докинга в стиле Visual Studio 2026. - -Библиотека является частью экосистемы **Lattice** и отвечает исключительно за математику макета, управление деревом узлов и контекстное состояние, не имея зависимостей от конкретных UI-фреймворков. - -## 🚀 Особенности - -- **Агностическая архитектура**: Полная совместимость с .NET 8+, WinUI 3 и Uno Platform. -- **Древовидная компоновка**: Управление интерфейсом через узлы (`Split` и `Content`). -- **Context-Aware System**: Встроенный сервис отслеживания контекста для динамического переключения панелей инструментов. -- **Smart Docking**: Алгоритмы автоматического разделения зон и схлопывания пустых контейнеров. -- **JSON Persistence**: Полиморфная сериализация макетов для сохранения и загрузки состояний пользователя. - -## 📁 Структура проекта - -* `Abstractions/` — Интерфейсы для расширения системы. -* `Models/` — Базовые сущности дерева (узлы, направления, ориентация). -* `Services/` - Сервисы управления интерфейсом - * `ContextService` - Сервис управления контекстом приложения. - * `LayoutService` - Сервис управления макетом. - * `NotificationService` - Сервис уведомлений. -* `Persistence/` — Логика сохранения макета в JSON. - -## 🛠 Использование - -### Создание базового макета - -```csharp -var layoutService = new LayoutService(); - -// Создаем контентные узлы -var explorer = new ContentNode(new MyToolComponent("Solution Explorer", "Explorer")); -var editor = new ContentNode(new MyDocumentComponent("Main.cs", "CodeEditor")); - -// Устанавливаем редактор как корень -layoutService.SetRoot(editor); - -// Прикрепляем проводник слева от редактора -layoutService.Dock(explorer, editor, DockDirection.Left); - -//Переключение контекста -var contextService = new ContextService(); - -// Вызывается при активации вкладки в UI -contextService.SetContext("CodeEditor"); - -// Проверка видимости команд в текущем контексте -bool isDebugVisible = contextService.IsCommandVisible("btnDebug", "CodeEditor"); -``` \ No newline at end of file diff --git a/Lattice.Core/Services/ContextService.cs b/Lattice.Core/Services/ContextService.cs deleted file mode 100644 index 4bc11f0..0000000 --- a/Lattice.Core/Services/ContextService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Lattice.Core.Abstractions; - -namespace Lattice.Core.Services; - -/// -/// Реализация сервиса управления контекстом приложения. -/// -public class ContextService : IContextService -{ - private string _currentContext = "Common"; - - /// - public string CurrentContext => _currentContext; - - /// - public event EventHandler? ContextChanged; - - /// - public void SetContext(string contextGroup) - { - if (string.IsNullOrWhiteSpace(contextGroup)) contextGroup = "Common"; - - if (_currentContext != contextGroup) - { - _currentContext = contextGroup; - ContextChanged?.Invoke(this, contextGroup); - } - } - - /// - public bool IsCommandVisible(string commandId, string commandContext) - { - // Базовая логика: команда видима, если её контекст совпадает с текущим - // или если команда помечена как общая ("Common" или "Global"). - return commandContext == "Common" || - commandContext == "Global" || - commandContext == _currentContext; - } -} diff --git a/Lattice.Core/Services/LayoutService.cs b/Lattice.Core/Services/LayoutService.cs deleted file mode 100644 index 3a833a5..0000000 --- a/Lattice.Core/Services/LayoutService.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Lattice.Core.Abstractions; -using Lattice.Core.Models; -using Lattice.Core.Models.Enums; -using Lattice.Core.Persistence; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Lattice.Core.Services; - -/// -/// Реализация сервиса управления макетом. -/// -public class LayoutService : ILayoutService -{ - private readonly ILogger? _logger; - private LayoutNode? _root; - - /// - public LayoutNode? Root => _root; - - /// - public event EventHandler? LayoutUpdated; - - public LayoutService(ILogger? logger = null) - { - _logger = logger; - } - - /// - public void Dock(LayoutNode source, LayoutNode target, DockDirection direction) - { - if (source == target) return; - - _logger?.LogDebug("Начало трансформации дерева: {Source} -> {Target} ({Direction})", source.Name, target.Name, direction); - - // 1. Извлекаем источник из его текущего места в дереве - Remove(source); - - // 2. Если докинг в центр — это логика объединения (например, в TabView) - // В рамках Core это может означать добавление в тот же контейнер - if (direction == DockDirection.Center) - { - HandleCenterDock(source, target); - } - else - { - // 3. Создаем разделение (Split) - HandleSideDock(source, target, direction); - } - - LayoutUpdated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Логика добавления элемента в центральную часть (вкладки). - /// - private void HandleCenterDock(LayoutNode source, LayoutNode target) - { - if (target.Parent is SplitContainerNode parent) - { - parent.AddChild(source); - } - else if (target == _root) - { - // Если таргет - корень, и мы докаем в центр, создаем контейнер по умолчанию - var container = new SplitContainerNode(SplitOrientation.Horizontal); - _root = container; - container.AddChild(target); - container.AddChild(source); - } - } - - /// - /// Логика разделения существующей области на две (Side Dock). - /// - private void HandleSideDock(LayoutNode source, LayoutNode target, DockDirection direction) - { - var orientation = (direction == DockDirection.Left || direction == DockDirection.Right) - ? SplitOrientation.Horizontal - : SplitOrientation.Vertical; - - var parent = target.Parent as SplitContainerNode; - - // Создаем новый сплиттер, который заменит target - var newContainer = new SplitContainerNode(orientation); - - if (parent != null) - { - // Заменяем target на новый контейнер в списке детей родителя - int index = parent.Children.IndexOf(target); - parent.Children[index] = newContainer; - newContainer.Parent = parent; - } - else - { - // Если родителя нет, значит target был корнем - _root = newContainer; - } - - // Настраиваем порядок в новом сплиттере - if (direction == DockDirection.Left || direction == DockDirection.Top) - { - newContainer.AddChild(source); - newContainer.AddChild(target); - } - else - { - newContainer.AddChild(target); - newContainer.AddChild(source); - } - - // Корректируем размеры (например, делим пополам) - source.WidthValue = 0.5; - target.WidthValue = 0.5; - source.IsWidthStar = true; - target.IsWidthStar = true; - } - - /// - public void Remove(LayoutNode node) - { - if (node.Parent is SplitContainerNode parent) - { - parent.Children.Remove(node); - node.Parent = null; - - // Если в контейнере остался один элемент — убираем лишнюю вложенность - if (parent.Children.Count == 1) - { - CollapseContainer(parent); - } - } - else if (node == _root) - { - _root = null; - } - - LayoutUpdated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Убирает ненужные контейнеры, если в них остался только один элемент. - /// - private void CollapseContainer(SplitContainerNode container) - { - var lastChild = container.Children[0]; - var parent = container.Parent as SplitContainerNode; - - if (parent != null) - { - int index = parent.Children.IndexOf(container); - parent.Children[index] = lastChild; - lastChild.Parent = parent; - } - else - { - _root = lastChild; - lastChild.Parent = null; - } - } - - /// - public string SaveLayout() - { - if (_root == null) return string.Empty; - - var options = GetJsonOptions(); - try - { - string json = JsonSerializer.Serialize(_root, options); - _logger?.LogInformation("Макет успешно экспортирован в JSON. Длина: {Length}", json.Length); - return json; - } - catch (Exception ex) - { - _logger?.LogError(ex, "Ошибка при сохранении макета Lattice"); - return string.Empty; - } - } - - /// - public void LoadLayout(string jsonData) - { - if (string.IsNullOrWhiteSpace(jsonData)) return; - - var options = GetJsonOptions(); - try - { - var importedRoot = JsonSerializer.Deserialize(jsonData, options); - if (importedRoot != null) - { - // При загрузке нужно восстановить связи Parent, так как они не сериализуются (циклические ссылки) - RestoreParentLinks(importedRoot, null); - _root = importedRoot; - _logger?.LogInformation("Макет успешно загружен. Корневой узел: {Id}", _root.Id); - LayoutUpdated?.Invoke(this, EventArgs.Empty); - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Ошибка при десериализации макета Lattice"); - } - } - - private JsonSerializerOptions GetJsonOptions() - { - return new JsonSerializerOptions - { - WriteIndented = true, - Converters = { new LayoutJsonConverter() }, - // Игнорируем циклы, так как мы восстановим Parent вручную - ReferenceHandler = ReferenceHandler.IgnoreCycles - }; - } - - private void RestoreParentLinks(LayoutNode node, LayoutNode? parent) - { - node.Parent = parent; - if (node is SplitContainerNode container) - { - foreach (var child in container.Children) - { - RestoreParentLinks(child, container); - } - } - } -} diff --git a/Lattice.Core/Services/NotificationService.cs b/Lattice.Core/Services/NotificationService.cs deleted file mode 100644 index 85ba200..0000000 --- a/Lattice.Core/Services/NotificationService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Lattice.Core.Abstractions; -using Lattice.Core.Models; -using Lattice.Core.Models.Enums; - -namespace Lattice.Core.Services; - -/// -/// Простая реализация сервиса уведомлений. -/// Хранит только событие и вызывает его при Show(). -/// -public sealed class NotificationService : INotificationService -{ - public event EventHandler? NotificationReceived; - - public void Show(string message, NotificationSeverity severity = NotificationSeverity.Info, int durationSeconds = 5) - { - NotificationReceived?.Invoke(this, new NotificationEventArgs(message, severity, durationSeconds)); - } -} \ No newline at end of file diff --git a/Lattice.IDE/App.xaml b/Lattice.IDE/App.xaml new file mode 100644 index 0000000..596d86a --- /dev/null +++ b/Lattice.IDE/App.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/Lattice.IDE/App.xaml.cs b/Lattice.IDE/App.xaml.cs new file mode 100644 index 0000000..d3d2778 --- /dev/null +++ b/Lattice.IDE/App.xaml.cs @@ -0,0 +1,37 @@ +using Lattice.Themes; +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Lattice.IDE +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + private Window? _window; + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + ThemeManager.Current.ApplyTheme(new FluentThemePack()); + + _window = new MainWindow(); + _window.Activate(); + } + } +} diff --git a/Lattice.IDE/Assets/LockScreenLogo.scale-200.png b/Lattice.IDE/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..7440f0d4bf7c7e26e4e36328738c68e624ee851e GIT binary patch literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(FqV6|IEGZ*x-#9g>~Mkr+x6^F zy~CDX2QIMs&Gcs3RnRBoxBA!*(Mfw0KTCYuYk0WlEIV>qBmPl! zq4ukrvfADX@#p8fbLY(H47N+k`FZ(FZh?cDro7>{8mkBO3>^oaIx`3!Jl)Qq)HI!+ z(S=1{o~eT)&W^=Ea8C`-17(Jv5(nHFJ{dOjGdxLVkY_y6&S1whfuFI4MM0kF0f&cO zPDVpV%nz;Id$>+0Ga5e9625-JcI)oq=#Pa3p^>8BB}21BUw@eN!-6@w%X+^`+Vn?! zryu|3T>kVWNBYyBc=7Y6H#s1Ah!OI_nezW zXTqOdkv2Az6KKBV=$yHdF^R3Fqw(TZEoNSZX>reXJ#bwX42%f|Pgg&ebxsLQ010xn AssI20 literal 0 HcmV?d00001 diff --git a/Lattice.IDE/Assets/SplashScreen.scale-200.png b/Lattice.IDE/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..32f486a86792a5e34cd9a8261b394c49b48f86be GIT binary patch literal 5372 zcmd5=Z){Ul6u)iv53sCbIJKLzl(EF%0tzcEY@|pLrfgF~2Dk$KFtU+$kbYqDN5W%7 z>?DBo!@y06eh{Oux>brrNT^{MO(tkiC@nH(2}}G_1|uvcMD(0{?|W^Gxo!tG~hW2Rn&7%b`-Kd_^`BCrb>XVtRKONoEw6%NswzMxk+kbocuk&}kJ#hSP z>8uR{r%LJ?I#)aaWW;uEixz+DzyTpp)MTEo&R%nEA92~g{^eXQwKV1m{xl5K<@k3FacT+Z zrwfy=VocIptI>t%@p5a;Rt=WXVnU;2SUdr7Yk>gw_2z_ICK^23$|Cg7{3Eg5j@N*F zetT?>30(*S_7ld-Yt&u7T{(hEjjM#vPlXibjrq?;pBBx3*>_2~VFGdsH5L zQKme_LAebV}aOX#+rQafZtp+4jK}V!>pn1?+eUH$0%6}z(Kul9!^2z zXi+d@jnx)RW7!j9uFEdv5N&1sCW#Z6Ej5Y7c;o28Q7i%U0(2v5J>o9P zl$#C8&9r)nL;?J65^GIeSOHYr3B7}}R~}@2Tx_xo5*YdU#g1bO}95cq69J!efdlE+xj1qG#ZUqh~1Sn#dBsZfDvcupM zXOFoyJ0$s+RHQKpzr#T>c&EUbq)lGvZDxuI!9unMI=#;ob2&gT)WqOjt6^X`_N21r`&eh6h0xpT!n6Z9rvE&+bFU$vTJO2? z#^tBNOx*2N)~(+TH8d>ep6``8V=3JEfdUUahVZ-xN+k#V&32x|%qnX(XBii5<@`%^ zV#Ky4f1!6RJqJXBU3M4~tmj2;;r`8_j&w?h5g35uMH(QI$Xpesb zG|*XRT?kh6M(jj0Y&vF^M*9g-iDMW%G%9%Pa}6ERQ9b0%6z1v}Ja=|L@G#5ZI>JS9 z*(K12nMvS?oyG8s9|q~{w`ajtI`KSHSiJ;)%X@M&eCE(VqI#F(XL?L@A$TUT?6av5 zkPWIR391XjSC%d6L}7F71Qpw(;c_~)mSZo-&Fm^FHlPX|Fu}1B3E+9j0}o1a(4HFS zUItE22CC%XZi!b4%~vWn>rpV9&CUEvt!?Q{Pr*L~51&(0Sz{VJJFrJtWw2PwXd|J{ zgH%3vAY$flodH=4&ruCHX;(3t;o}n?!0~3EE|5qRz$!VIkphxa4@_jyfiE9m;0 zjcYJ2;26N&MTB8X4joZ&?SUe|VS$^I%dt{!c2O;%3SdqW@K_14r8eyC1s&VcU5+2~ z_O1Cc*w|aIA=VC6AT_EFoL}W#Rl;7CZe)e}RS*e;8CVyM6i8a(yO@|S709VYY(y2g zc+QxB>Bw^B^2Db~*o)=i$m-aUNQFkYy5(eJW$cez>C{POds*p3cy#tHnvActP;dBP zdEf)C;lq}&#PE?XCD<~ngrzYUg|nS`#MS`Rd7cT>xlR19P#~4Qg5!J}@glCUq)z_2 zjvyv%aSq0 z)njao1dV0XNw&c@qmj1e*jgQ$l@_urW5G4RSY#rT1z`#%3;{EB`aJK|TH^lb_3nAT z-_Q4X-(K&IS8UyqsnjYdippfmN-HT!X2MT;Dpcy~-#$k6V z|MR4vU#O&p7TC46pTflb3 zoUJ;ZRf#&8&EwXy5s%!&(q6cN62swD#FH%O-RJsjWPZN3^^@FCIQ&MxXIFo7!I#VI zkpIstuWqUV5uhgs07?k$*!`uiZ=5b#$lI|0c+XJvj(}zSE3MN#EyOK zql(#yA}~Ibl*r(s1}Z^5mmn*-n93g?-ccM+^PN?6HH~h0hjy6@XY*^i<-V)+OZ;p7 z7j`p_sT55xnYsedNIIel^QIIg7i@`2Qi}x5$!tk29$2OQI zs^kQXAKE}5ZJu$)2@Dxn?}}O@f@6@^!%9Tj+o>=jd!^ZuvBE4jb4g}Z5WMBtcmy^~ zoFGVS5|0FA!(1Q%fL?Bj*L+9ZL{mjSO8lzqrQ0UCZ)X zPwk$1HNFgaK%NxGpuXz}#ywXvf2JQ?BQ5uOZM2up4S#ieaxS$!o9o6Z=czNQb} zwAh|xLZ>+WyN%o?^uCAQw&&4o?S$DJ`WP(Hr*grL*qNXlqU0osCQ(Up5F(^$Z5;n&oJIO4uF`k&QL*j{f zU=;#MZ5{@b%qMbjTB3dh-5#mqY>%{0jgS+WdHyG literal 0 HcmV?d00001 diff --git a/Lattice.IDE/Assets/Square44x44Logo.scale-200.png b/Lattice.IDE/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..f713bba67f551ef91020b75716a4dc8ebd744b1c GIT binary patch literal 637 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL1|$oo8kjIJFu8cTIEGZ*dUI*J;2{SImxtDO zm%3!R$UazoY}x{$j0P5ABYXWr(l=jxJ6ps1W{tV=^>{Dl><3nv3A}sm=EZ)#l3`NR zpZda3^rNox*D1%NC98Z~L*6zipLw~Gxn&(Y-;KmJ+aR6eLabU-L#y8HW%7P-E_-VlLqIabbHPHKT*)fT@9iWJ7iWgOT9%0}Lrj>lztPxWq6sPw3pi z#-<=#$jjrP_DD*i!RLsn0mIA=>4~N)IMYWIf=j%-zuKCdMG%tHYot70D1| zvWa0wMhauW#S>1CnI_;>!1Q3zMA17@DOVq{MQ+{U7^a&yA+%dMCG;WNPV0i;w$tu; zX^b}UKziPM)(<;)ruW;-`)bBN+rQNM*Zs_>?n$|FVFo-e*PZb*@U7VAd+tHb4e?=Blc~}S6K)wL}r*Gf`BM#QB z+y>N$mCswb4d{^{S9v_!eQj4fTRMOwOCi?lSk9%<=vAz}jM-*PQtH@Odn1LZcd^j#o> hW$4xn+CT+ep9lJ{OAO?njobhL002ovPDHLkV1nYebbkN< literal 0 HcmV?d00001 diff --git a/Lattice.IDE/Assets/StoreLogo.png b/Lattice.IDE/Assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a4586f26bdf7841cad10f39cdffe2aca3af252c1 GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fF!p=8IEGZ*dUM0H=rDtTTVkd2 z(%lbKn@VS_lUaADVB&;Z6F#LM+mPsa?e>FnHo;HND^!P`-lX%BH~FOg%y&x+t*x!? zg$#_1A1kgsSvO(fw`bOmo;lrJX8byO1j^gf7qohR%mmt z@L)WX;>gqgK|tWJvQ5j;4;=gt4HXVKSMYRv5RhY5vS~TqfK_NAP*r{h!!g^BZ;w4r z7CGdsai)y;fJQc`7{Zc2b==h%o`Op$|bg6a&nL{*m7-=0>k4M4-PXlU;G-?%*(*g>iFt^ U$m#7DfHB12>FVdQ&MBb@0G`#n8vpc0sq%A~kJcD9FY~qQRMt?ZR3YyDZt}Od;|mgpc{2dv9AHF){kXU%k({ z=Y8JidEayHTkG@twPZ|U3_^%3ct-OgLSiFAqDN!|tbCX@c@?4P`2x*TMK!+Q4b?k0 ziW7!!KF6dPWcF<%I|iznM~`QJ_V7sHGV_D`dhgpA9Vd@&X}ErK+j~_rdv;Bp?OA@a zFXOk7eWOJe5NcK;6h$FaM&7JxNc#-@QTwzW6x#d_zmQNkz5) zPI;kh;3d;5UCJU+9a(cOxX(|edWoOiAEdGU#kPJ&xnc2||3vDbuhBCkj-pb0as$Zl z5;}4n=**n6(1g`JEtSy;SG6X;#-F~Oz3lESG2b5`j@wAwY4Yp<=4Xeb>iH=6aicF?DxD&q{`!&}ct zBI)aycwuobQAf&678Uf+Mmh-@9RUhyH~>?w0dixO0#jZjEc9R^=5NZw=|a(kcB?9^ zfnTiEFXp-q#B;Tn>(O%$A*ud^Rg&eVH6Y_5Y%!E39RR&s?XpG`gKwU!6FE1 z7X)DC7)*(5g}lh`4`{i~DZcWupZI`K)_4P)VE{@gc7@Xsd^86zl~_mOYH?I4!aGeX z^E(_=L6?PgveDQ+r%P@UISEXrkn`LHJZ##+!-anV>6h)IkKp;E@p8+3&(5%kS2)ld*J*rJccZM0iyaAx7+F~GW1UWFK&3X$PE1^}NH zgAG9ck5K!{07OwU@j@Do>TbH=CDEo#4m0cEyAuXy_<&jlzJVcKweSJ5 z&=q~iIn18$w8yb=rmEmHxVEUA^?RwnB?6Qlp1os8@*dWTGL2bhzZ!s*xqScR?EPL` zo(JwNdKUUYy7GtvZ3asXm)cgFvCx9EmAi;|w=a0iGiv%%VYKh`P0Wma4y`Xyx|T~( zAmfGbgbEEC7)j8b@WA@+5W3a61HJXC1dX@6_T|Czk0I0zBk%tnW~()VWITGI!`$c< gARL?UBrYYkwoDw4eo*CrzXGTrZ@;GF>596)00d&n@&Et; literal 0 HcmV?d00001 diff --git a/Lattice.IDE/Controls/EditorView.xaml b/Lattice.IDE/Controls/EditorView.xaml new file mode 100644 index 0000000..4d3e5e5 --- /dev/null +++ b/Lattice.IDE/Controls/EditorView.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + using System; +using Lattice.Core; + +namespace Lattice.IDE.Demo; + +public class Program +{ + public void Main() + { + Console.WriteLine("Hello, Lattice 2026!"); + } +} + + + + \ No newline at end of file diff --git a/Lattice.IDE/Controls/EditorView.xaml.cs b/Lattice.IDE/Controls/EditorView.xaml.cs new file mode 100644 index 0000000..1f45a18 --- /dev/null +++ b/Lattice.IDE/Controls/EditorView.xaml.cs @@ -0,0 +1,28 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Lattice.IDE.Controls +{ + public sealed partial class EditorView : UserControl + { + public EditorView() + { + InitializeComponent(); + } + } +} diff --git a/Lattice.IDE/Controls/SolutionExplorerView.xaml b/Lattice.IDE/Controls/SolutionExplorerView.xaml new file mode 100644 index 0000000..6216bb7 --- /dev/null +++ b/Lattice.IDE/Controls/SolutionExplorerView.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lattice.IDE/Controls/SolutionExplorerView.xaml.cs b/Lattice.IDE/Controls/SolutionExplorerView.xaml.cs new file mode 100644 index 0000000..ed3a3e8 --- /dev/null +++ b/Lattice.IDE/Controls/SolutionExplorerView.xaml.cs @@ -0,0 +1,28 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Lattice.IDE.Controls +{ + public sealed partial class SolutionExplorerView : UserControl + { + public SolutionExplorerView() + { + InitializeComponent(); + } + } +} diff --git a/Lattice.IDE/Lattice.IDE.csproj b/Lattice.IDE/Lattice.IDE.csproj new file mode 100644 index 0000000..57f9197 --- /dev/null +++ b/Lattice.IDE/Lattice.IDE.csproj @@ -0,0 +1,81 @@ + + + WinExe + net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0 + 10.0.17763.0 + Lattice.IDE + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + win-$(Platform).pubxml + true + false + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + true + + + + + False + True + False + True + + \ No newline at end of file diff --git a/Lattice.IDE/Layout/DemoContent.cs b/Lattice.IDE/Layout/DemoContent.cs new file mode 100644 index 0000000..44099e2 --- /dev/null +++ b/Lattice.IDE/Layout/DemoContent.cs @@ -0,0 +1,28 @@ +using Lattice.Core.Docking.Abstractions; + +namespace Lattice.IDE; + +/// +/// Реализация контента для демонстрации, принимающая любой UI-объект. +/// +public class DemoContent : IDockContent +{ + public string Id { get; } + public string Title { get; set; } + + /// + /// Сюда мы передаем наш UserControl (SolutionExplorerView и т.д.) + /// + public object View { get; set; } + + public bool CanClose { get; set; } = true; + + public DemoContent(string id, string title, object view) + { + Id = id; + Title = title; + View = view; + } + + public bool OnClosing() => true; +} diff --git a/Lattice.IDE/MainWindow.xaml b/Lattice.IDE/MainWindow.xaml new file mode 100644 index 0000000..1c8e9e7 --- /dev/null +++ b/Lattice.IDE/MainWindow.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + +