Giter Club home page Giter Club logo

stridecomponentseditoravalonia's Introduction

Experimental component editor for Stride in Avalonia

My idea for this weekend the month of October is to try creating a very simple component editor for Stride.

Tasks

  • Open a project file
  • Select a scene
  • View the entity hierarchy
  • Select an entity and open its components in a panel
  • View components properties
  • Edit components properties
  • Save the modified scene

Enhancements

  • Make Undo/Redo more usable (currently too many events are registered)
  • Figure out a sane docking system implementation
  • Better documentation
  • Styling
  • Custom views for:
    • Vector2
    • Vector3
    • Vector4
    • Quaternion
    • Color3
    • Color4
    • Collections (lists/arrays/dictionaries)
    • Entity References
    • EntityComponent References
    • Abstract members (i.e. interface/abstract class implementations)
    • Asset References
  • Member inlining
  • Drag and drop entities onto component members with compatibility checking
  • Hiding members with [Display(Browsable = false)] and bool Enabled
  • Use singleton pattern for views and commands to avoid allocations

Architecture

The project has been divided into main 5 parts:

  • Design - abstract interfaces and view models,
  • Commands - methods that modify the view models or call services,
  • Services - more complex classes that manage objects in the application,
  • Presentation - views for the view models,
    • Virtual - a wrapper over Avalonia.FuncUI to allow writing MVU-like views,
  • Editor - initialization logic and anything that has to touch both service logic and presentation.

This kind of horizontal division has been used to impose strict dependencies (e.g. most services are UI independant and should not reference UI logic directly, only through some abstraction). In practice, however, it would often be easier to have just 1-2 projects.

Plugins

Everything should be a plugin was the motto.

  • Menu items are registered in IMenuProvider
  • Member views (for property grid) are registered in MemberViewProvider
  • Views are registered in ViewRegistry
  • Asset Editors are registered in IAssetEditorRegistry

There's not yet a mechanism for loading external plugins, but it shouldn't be hard to implement.

Views

The views are stateless classes with a method that takes a view model and returns a virtual view object. Later this virtual view is applied to the actual UI system.

Commands

Commands are stateless classes that require to be passed a context with data to execute. Commands are executed only by the CommandDispatcher, which processes commands after the UI thread finishes processing user input and then updates the view.

stridecomponentseditoravalonia's People

Contributors

manio143 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

stridecomponentseditoravalonia's Issues

Implement MemberView

Expansion of structured members is determined by the view (e.g. Vector3 is not expandable, but user defined struct is)
The view model can have bool? IsExpanded.
Follow the registration of Member Views for further plugins.

Context aware menu item

Dependent on the last selected Asset Editor a different menu item may be present with actions specific to that Asset Editor.

This will extend the Menu Provider.

Menu update

Currently the menu doesn't get updated. This needs investigation.
A possible fix - which is something I was targeting anyway - is to introduce menu generation from a view model.

[Plugin] AttachedSceneEditor

Attached Scene Editor

This plugin is meant to allow launching the user's game from within the Stride.Editor, such that there's no need for an external scene monitor (like StrideLiveEditor).

To use the plugin we would want to either:

  • Provide user with an interface to implement which returns an instance of IGame, OR
  • Ask user to select an implementation of GameBase with a new() constructor that would be ran.

The second option may be better, as it allows us to provide the game with a GameContext and potentially render it within an Avalonia controlled window.

Consequently, the plugin would subscribe the NextFrame() async event and rebuild it's view model and refresh the view, accessing the SceneInstance game system for the source of entities.

Make master pass build in GH Actions

Right now I'm using a dev build of Stride and FuncUI so that I can debug them properly.
I can use the FuncUI from Nuget now, since I fixed most issues I had with it.
I'm depending on one custom feature in my Stride build that is currently in a PR review. For now the Nuget gets resolved to an earlier official release which is ok, because my custom feature is passive.
To be able to build on linux I also need to fix stride3d/stride#757 and it seems that Stride.Core.Assets has a dependency on Microsoft.WindowsDesktop.App framework which doesn't exist on linux - this might be related to the use of MSBuild.Sdk.Extras, but at this point I'm not sure.

Add tooltips to members from XML documentation

Reading the XML doc format is explained here: https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection

We can make a similar class to this: https://github.com/stride3d/stride/blob/master/sources/editor/Stride.Core.Assets.Editor/Services/UserDocumentationService.cs

Then add public string Tooltip { get; } to the MemberViewModel, which in its ctor would get and call DocumentationService.
Note: this will impose a dependency on IServiceRegistry across all view models that create members.

ContainerControl within the editor tab is not properly updated

The ViewDataTemplate as IViewUpdater performs an update correctly on the cached container control, however the change isn't correctly propagated to the visual tree of the EditorTabViewModel - likely an issue with the docking library.

You can verify that the Tag on the container is changed in our update, but not updated when viewing the window with diagnostics tool (F12).

Moving tabs around does not work

Undocking a tab doesn't create a window with it.
Redocking a tab to another dock causes an unrecoverable exception - might this be connected to the reuse of ContentControl in ViewDataTemplate (if so, how to detect potential parent change in its Build(object)?)

 System.InvalidOperationException: The control already has a visual parent.
   at Avalonia.Visual.ValidateVisualChild(IVisual c) in D:\a\1\s\src\Avalonia.Visuals\Visual.cs:line 506
   at Avalonia.Collections.AvaloniaList`1.Add(T item) in D:\a\1\s\src\Avalonia.Base\Collections\AvaloniaList.cs:line 190
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild() in D:\a\1\s\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 265
   at Avalonia.Controls.ItemsControl.OnContainersMaterialized(ItemContainerEventArgs e) in D:\a\1\s\src\Avalonia.Controls\ItemsControl.cs:line 247
   at Avalonia.Controls.Primitives.SelectingItemsControl.OnContainersMaterialized(ItemContainerEventArgs e) in D:\a\1\s\src\Avalonia.Controls\Primitives\SelectingItemsControl.cs:line 362
   at Avalonia.Controls.ItemsControl.<get_ItemContainerGenerator>b__12_0(Object _, ItemContainerEventArgs e) in D:\a\1\s\src\Avalonia.Controls\ItemsControl.cs:line 94
   at Avalonia.Controls.Generators.ItemContainerGenerator.Materialize(Int32 index, Object item) in D:\a\1\s\src\Avalonia.Controls\Generators\ItemContainerGenerator.cs:line 64
   at Avalonia.Controls.Presenters.ItemContainerSync.AddContainers(ItemsPresenterBase owner, Int32 index, IEnumerable items) in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemContainerSync.cs:line 91
   at Avalonia.Controls.Presenters.ItemContainerSync.<ItemsChanged>g__Add|0_0(<>c__DisplayClass0_0& ) in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemContainerSync.cs:line 33
   at Avalonia.Controls.Presenters.ItemContainerSync.ItemsChanged(ItemsPresenterBase owner, IEnumerable items, NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemContainerSync.cs:line 43
   at Avalonia.Controls.Presenters.CarouselPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\Presenters\CarouselPresenter.cs:line 102
   at Avalonia.Controls.Presenters.ItemsPresenterBase.Avalonia.Controls.Presenters.IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemsPresenterBase.cs:line 156
   at Avalonia.Controls.ItemsControl.ItemsCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\ItemsControl.cs:line 394
   at Avalonia.Controls.Primitives.SelectingItemsControl.ItemsCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\Primitives\SelectingItemsControl.cs:line 321
   at Avalonia.Collections.NotifyCollectionChangedExtensions.<>c__DisplayClass1_0.<WeakSubscribe>b__0(NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Base\Collections\NotifyCollectionChangedExtensions.cs:line 43
   at Avalonia.Reactive.LightweightObservableBase`1.PublishNext(T value) in D:\a\1\s\src\Avalonia.Base\Reactive\LightweightObservableBase.cs:line 132
   at Avalonia.Collections.NotifyCollectionChangedExtensions.WeakCollectionChangedObservable.OnEvent(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Base\Collections\NotifyCollectionChangedExtensions.cs:line 77
   at Avalonia.Utilities.WeakSubscriptionManager.Subscription`1.OnEvent(Object sender, T eventArgs) in D:\a\1\s\src\Avalonia.Base\Utilities\WeakSubscriptionManager.cs:line 187
   at Avalonia.Collections.AvaloniaList`1.NotifyAdd(T item, Int32 index) in D:\a\1\s\src\Avalonia.Base\Collections\AvaloniaList.cs:line 582
   at Avalonia.Collections.AvaloniaList`1.Insert(Int32 index, T item) in D:\a\1\s\src\Avalonia.Base\Collections\AvaloniaList.cs:line 297
   at Dock.Model.FactoryBase.MoveDockable(IDock sourceDock, IDock targetDock, IDockable sourceDockable, IDockable targetDockable) in D:\a\1\s\src\Dock.Model\FactoryBase.cs:line 581
   at Dock.Model.DockManager.MoveDockable(IDockable sourceDockable, IDock sourceDockableOwner, IDock targetDock, Boolean bExecute) in D:\a\1\s\src\Dock.Model\DockManager.cs:line 51
   at Dock.Model.DockManager.DockDockable(IDockable sourceDockable, IDock sourceDockableOwner, IDock targetDock, DockOperation operation, Boolean bExecute) in D:\a\1\s\src\Dock.Model\DockManager.cs:line 253
   at Dock.Model.DockManager.DockDockableIntoDock(IDockable sourceDockable, IDock sourceDockableOwner, IDock targetDock, DragAction action, DockOperation operation, Boolean bExecute) in D:\a\1\s\src\Dock.Model\DockManager.cs:line 277
   at Dock.Model.DockManager.DockDockableIntoDockVisible(IDock sourceDock, IDock targetDock, DragAction action, DockOperation operation, Boolean bExecute) in D:\a\1\s\src\Dock.Model\DockManager.cs:line 286
   at Dock.Model.DockManager.ValidateDock(IDock sourceDock, IDockable targetDockable, DragAction action, DockOperation operation, Boolean bExecute) in D:\a\1\s\src\Dock.Model\DockManager.cs:line 392
   at Dock.Avalonia.DockControlState.Execute(Point point, DockOperation operation, DragAction dragAction, IVisual relativeTo) in D:\a\1\s\src\Dock.Avalonia\DockControlState.cs:line 109
   at Dock.Avalonia.DockControlState.Drop(Point point, DragAction dragAction, IVisual relativeTo) in D:\a\1\s\src\Dock.Avalonia\DockControlState.cs:line 67
   at Dock.Avalonia.DockControlState.Process(Point point, Vector delta, EventType eventType, DragAction dragAction, IVisual activeDockControl, IList`1 dockControls) in D:\a\1\s\src\Dock.Avalonia\DockControlState.cs:line 181
   at Dock.Avalonia.Controls.DockControl.Released(Object sender, PointerReleasedEventArgs e) in D:\a\1\s\src\Dock.Avalonia\Controls\DockControl.cs:line 96
   at Avalonia.Interactivity.Interactive.RaiseEventImpl(RoutedEventArgs e) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 206
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.HierarchyTraverser`2.Traverse(IInteractive target) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 323
   at Avalonia.Interactivity.Interactive.TunnelEvent(RoutedEventArgs e) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 185
   at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 140
   at Avalonia.Input.MouseDevice.MouseUp(IMouseDevice device, UInt64 timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) in D:\a\1\s\src\Avalonia.Input\MouseDevice.cs:line 277
   at Avalonia.Input.MouseDevice.ProcessRawEvent(RawPointerEventArgs e) in D:\a\1\s\src\Avalonia.Input\MouseDevice.cs:line 128
   at Avalonia.Input.InputManager.ProcessInput(RawInputEventArgs e) in D:\a\1\s\src\Avalonia.Input\InputManager.cs:line 40
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in D:\a\1\s\src\Windows\Avalonia.Win32\WindowImpl.cs:line 762
   at Avalonia.Win32.Interop.UnmanagedMethods.DispatchMessage(MSG& lpmsg)
   at Avalonia.Win32.Win32Platform.RunLoop(CancellationToken cancellationToken) in D:\a\1\s\src\Windows\Avalonia.Win32\Win32Platform.cs:line 121
   at Avalonia.Threading.Dispatcher.MainLoop(CancellationToken cancellationToken) in D:\a\1\s\src\Avalonia.Base\Threading\Dispatcher.cs:line 65
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[] args) in D:\a\1\s\src\Avalonia.Controls\ApplicationLifetimes\ClassicDesktopStyleApplicationLifetime.cs:line 106
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime[T](T builder, String[] args, ShutdownMode shutdownMode) in D:\a\1\s\src\Avalonia.Controls\ApplicationLifetimes\ClassicDesktopStyleApplicationLifetime.cs:line 128
   at Stride.Editor.Program.Main(String[] args) in D:\Documents\Stride Projects\StrideComponentsEditorAvalonia\Stride.Editor\Program.cs:line 17

Use AssemblyRegistry for plugin discovery

In PluginRegistry.RefreshAvailablePlugins() I've made a foreach loop on AppDomain.CurrentDomain.GetAssemblies(). This however means I'm iterating over many assemblies which guaranteed will not contain any plugins and may have a lot of types. Therefore I will use a custom category "editor" for registering assemblies in the Stride.Core.Reflection.AssemblyRegistry.

The registration has to happen in the module initializer which is called upon Assembly.Load() (for direct dependencies referenced in the assembly header this is called before entry point).

internal class Module
{
    [ModuleInitializer]
    public static void Initialize()
    {
        AssemblyRegistry.Register(typeof(Module).Assembly,
            Stride.Core.Reflection.AssemblyCommonCategories.Assets,
            Stride.Editor.Design.EditorAssemblyCategory.Editor);
    }
}

Investigate: why ViewBuild.Equals returns false, but FuncUI doesn't

Right now I'm only running updateRoot if my Equals function returned false, which means FuncUI has to see a change and update. But for some reason it doesn't.

See diffAttributes (L117-L122) for where the check occurs. The only implementation of IAttr is Attr<'viewType> which has structural equality. Then we also have default structural for ViewContent, View<'viewType> and others. Content equality works or sure as during debugging I was getting inside nested View<TreeViewItem>. But somehow my change in bool IsSelected isn't picked up.

The issue may very well be somewhere in my code, but I couldn't find it for now. Let's give it a day or two and I'll put some fresh eyes on it. I will create a smaller game test project to check these things.

Refactor commands dispatching

I need to rethink how commands are dispatched, executed, registered in undo/redo and then cause view refresh.
Currently it's a mess, because I made some initial assumptions that stopped working.

View dispatches a command to Dispatcher

  • command gets executed
    • reversible - registered in Undo/Redo
    • non-reversible
  • view gets updated

Undo is called

  • command gets re dispatched with a context ?
  • dispatcher recognizes that it should re-register command
  • dispatcher executes undo
  • view gets updated

Suppress command dispatching during view building

FuncUI executes subscription methods when creating a control, not just when patching it later. This leads to unnecessary commands being dispatched initially, and furthermore invalid reversible commands registering in undo/redo.

Fix: add a state to dispatcher (bool + lock object) that is set during view update - this also means that initial view update should be a result of dispatching a InitializeEditorCommand which does nothing, but forces view update.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.