A recent update allows support for plugins, either core projects or dynamically loaded plugins.
These are not really plugins, but instead modules. They are projects referenced by the main project and are loaded before dynamic plugins.
For example, the FFmpegMedia plugin is a core plugin, and implements exporting
and playback of media files. This core plugin is only loaded when FFmpeg is available
These are plugins that are completely dynamically loaded. This system is still WIP, but they can do everything a core plugin can do.
Assembly loading doesn't use an AssemblyLoadContext
, so there's versioning issues to deal with
The only dynamic plugin is CircleClipPlugin
, which is a test plugin which adds a video clip that draws a circle.
So far, plugins are able to do a fair amount of things, such as:
- Registering custom exporters (see FFmpegMedia plugin which adds an FFmpeg exporter, to export .mp4 files)
- Custom configuration pages (still slightly WIP; no built-in way to save data to disk)
- Custom data parameters and property editor slots (aka rows)
- Custom clips (see FFmpegMedia plugin which adds a video clip to play videos)
- Custom commands
- Adding context menu entries to clip, track and resource context menus
- Resource drop-in-timeline handling
- Handling native files dropped in resource manager
... And a few others. There's still lots that cannot be done yet. A few of the main things are:
- Cannot add menus to the editor's top-level menu
- Custom panels in the UI
- Access to the keyframe UI system, thought I can't see why a plugin would want to access this
This section describes the main APIs of FramePFX. This is still being updated
The property editor system is a highly customisable framework, which also makes it somewhat complicated to use.
The main power of the custom property editor system is supporting multiple objects being modified at once. We call these "Handlers".
Say for example you want to edit a double
property in multiple objects, and:
- The property has a fixed value range (e.g. 0.0 to 1.0)
- You want to post-process the displayed value string (e.g. display 0.0 to 1.0 as a percentage: 0% to 100.0%)
- You also want to support manual value entry and support converting, say, 95% back to a raw value of 0.95 for the property
- Change the value increments (aka step profile) of the NumberDragger based on if shift, ctrl or both are pressed.
You can do all of this on a single line... sort of! There's lots of built-in classes which make this doable on a single line.
The property editor consists of rows that we call "Slots". A slot typically consists of a left, spacing, and right column; left for the display name, and right for the slot content (e.g. a slider, NumberDragger or text box). The column sizes are bound to the PropertyEditorControl.Column[0,1,2]Width
properties.
For example, this is the property editor slot used to modify the Line Spacing
of a text clip, which is also animatable:
new ParameterFloatPropertyEditorSlot( // The slot which manages animatable float parameters
TextVideoClip.LineSpacingParameter, // The specific parameter we want to modify
typeof(TextVideoClip), // The type of object the slot is applicable to (multiselection requires this)
"Line Spacing", // A readable display name string, on the left column
DragStepProfile.Pixels) // The NumberDragger's increment behaviour. Pixels is fine tuned
{
// Converts, say, "25.3" into "25.3 px", and supports converting back too "25.3"
ValueFormatter = SuffixValueFormatter.StandardPixels
});
This example is for an automatable parameter though, and you might not need animatable parameters, in which case, you can use DataParameterFloatPropertyEditorSlot
for a DataParameterFloat
.
You can register a custom property editor slot's UI control via the BasePropertyEditorSlotControl.Registry
, like so:
// The model/control naming convention isn't strict, it's just good practice
Registry.RegisterType<MyCustomPropertyEditorSlot>(
// ModelControlRegistry<TModel, TControl> uses factory methods
() => new MyCustomPropertyEditorSlotControl()
);
Slot controls may be recycled, so it's important to override the OnConnected
and OnDisconnected
methods and add/remove event handlers or bindings accordingly to and from the page model.
A lot of the system is documented in code, so for specific things like slot selection and multiple
handlers with differing values, I recommend reading the source code to get a better understanding
To specify the handlers that a slot can modify, you call SetHandlers(IReadOnlyList<object>)
on the slot, and, if the slot supports the number of
handlers and also the underlying types of the handler object(s), then it sets IsCurrentlyApplicable
to true, and the slot can work as normal,
and the Handlers
property is updated with the objects that can be modified. ClearHandlers
will obviously clear the handlers and the slot is no longer applicable.
FramePFX provides two parameter subsystems: data parameters, and automatable parameters.
Data parameters are represented by the base class DataParameter
, but you shouldn't override this class directly, instead override DataParameter<T>
if you need your own type. These parameters are used to make interfacing with a PropertyEditor
or binding to the UI in code-behind generally easier; it saves having to use reflection everywhere by instead going through a ValueAccessor<T>
.
Automatable parameters have a more specific usage, in that they are just keys to a dictionary of automation sequences. The file docs/Automation.md
has more information about the automation system. Automatable parameters, like data parameters, can also be used with property editors, and the standard automation property editor slots provide insert key frame/toggle override/reset value commands, which saves doing it manually.
You can find live templated for JetBrains Rider that allow you to easily define data parameters and automatable parameters in the LIVETEMPLATES.md
file in the solution.
Configuration pages are the standard way of modifying application or project properties.
Page models are implemented via the ConfigurationPage
class. This base class contains the active ConfigurationContext
which is an object created whenever the settings dialog is opened and is used to track modified pages and the active page (currently being viewed).
The methods available are:
-
ValueTask OnContextCreated(ConfigurationContext)
: This is invoked recursively for every page in a configuration manager when a settings dialog is opened. This is where you can load data from the application or project into the state of the page, and also register event handlers for data changes, if you need to. -
ValueTask OnContextDestroyed(ConfigurationContext)
: This is invoked recursively for every page in a configuration manager when a settings dialog is closed. You should unregister event handlers in this method -
ValueTask Apply(List<ApplyChangesFailureEntry>)
: Apply changes back into the application or project. The provided list is not fully implemented yet, however, it should be used instead of showing message dialogs, since it might annoy the user if there's 100s of errors that occur. So instead, all errors will be shown at once in a custom dialog using theApplyChangesFailureEntry
objects as the models. -
void OnActiveContextChanged(ConfigurationContext, ConfigurationContext)
: This is invoked when the viewed configuration page changes.newContext
is null when this page is no longer visible, and is non-null when this page is now being viewed. You may wish to implement the loading data behaviour in this method instead ofOnContextCreated
to help with performance. But beware, this method isn't async, since it is invoked during a UI input event (the tree node being clicked), so don't do anything too slow here
There's a singleton configuration page for the entire application stored in ApplicationConfigurationManager.Instance
. You can add your own configuration entries and pages in your plugin's OnApplicationLoading
method
Project settings are stored in a ProjectConfigurationManager
.
Since there's a configuration manager for each instance of a project, you must listen to the ProjectConfigurationManager.SetupProjectConfiguration
event, and add your own configuration entries and pages to the ProjectConfigurationManager
given as an event parameter.
The simplest way to create a configuration page would be to derive from PropertyEditorConfigurationPage
and use its property editor
But if you wish to implement a completely custom configuration page control (either XAML or declarative if you so please), you can register a mapping via the ConfigurationPageRegistry.Registry
, like so:
Registry.RegisterType<MyConfigurationPage>(
() => new MyConfigurationPageControl()
);
By doing this, you allow the UI to create your control when it tries to present your page. Page controls may be recycled, so it's important to override the OnConnected
and OnDisconnected
methods and add/remove event handlers or bindings accordingly to and from the page model.
First you need a class that derives BaseExporterInfo
. This base class defines the standard information and behaviours for exporters.
This class then needs to override the CreateContext
method, which creates an export context object. This object should derive from BaseExportContext
.
This class can then implement the Export
method, which is what actually does the exporting (including creating the file and writing media information)
Exporters might want adjustable parameters to change the export process (e.g. bitrate or resolution). Currently, this can
only be done via the PropertyEditor
instance defined in BaseExporterInfo
. You can define data parameters (or use custom property editor slots),
and register them in the property editor. The front end will automatically create standard UI controls for built-in property editor slots.
The ExportSetup
class contains basic information about the export process, which is common across every exporter (e.g. the start and end frame to export, and target file/directory)
Finally, you register the exporter via the ExporterRegistry
(accessible by the Instance
static property),
and by invoking RegisterExporter(ExporterKey, BaseExporterInfo)
. An exporter key contains a unique key and display name for the exporter
Creating a custom clip is very simple. Your clip should derive from VideoClip
or AudioClip
(audio is currently not working, since a rework of the rendering system is needed to make it work property).
Video clips have two main methods: bool PrepareRenderFrame(PreRenderContext rc, long frame)
and void RenderFrame(RenderContext rc, ref SKRect renderArea)
.
The prepare render frame method indicates to the rendering system whether or not the clip should be rendered. For example, if a resource reference required by the clip is not linked, then this method returns false. This method is invoked on the main thread
The render frame method is invoked on a background rendering thread, and is what should render the clip. You can access the skia canvas via rc.Canvas
. the ref renderArea
is used to tell the rendering system the affected pixel area, as an optimisation. For example, if you draw a 10x10 square starting at 5,5, then you would do: renderArea = rc.TranslateRect(new SKRect(5, 5, 15, 15));
. The method rc.TranslateRect
translates the rect into the effective rectangle based on the current TotalMatrix
of the canvas
You can register custom commands in your plugin's RegisterCommands
method, like so:
public override void RegisterCommands(CommandManager manager) {
manager.Register(
"myplugin.commands.editor.ShowCompTimlineName",
new ShowCompTimlineNameCommand()
);
}
private class ShowCompTimlineNameCommand : Command {
protected override Executability CanExecuteCore(CommandEventArgs e)
{
if (!DataKeys.TimelineKey.TryGetContext(e.ContextData, out Timeline? timeline)) {
// Normally, MenuItem will be invisible, or button will be disabled
return Executability.Invalid;
}
return timeline is CompositionTimeline
? Executability.Valid // Control clickable
: Executability.ValidButCannotExecute; // Control disabled
}
protected override async Task ExecuteAsync(CommandEventArgs e)
{
if (!DataKeys.TimelineKey.TryGetContext(e.ContextData, out Timeline timeline))
return;
if (!(timeline is CompositionTimeline composition))
return;
await IMessageDialogService.Instance.ShowMessage(
"hello", $"My resource = '{composition.Resource.DisplayName}'"
);
}
}
This section describes the colour brush and icon API. This provides a way for plugins to create icons and use them in different parts of the application without having to ever interact with avalonia bitmaps or images directly.
This service provides a mechanism for creating abstract delegates around Avalonia brushes.
CreateConstant
: creates the equivalent ofImmutableSolidColorBrush
GetStaticThemeBrush
: creates the equivalent ofIImmutableBrush
GetDynamicThemeBrush
is more complicated. It's a subscription based object where the front end subscribes to dynamic changes of a brush via the application'sResourcesChanged
andActualThemeVariantChanged
events. This allows, for example, an icon to use the standard glyph colour (which is white within dark themes and black within light themes, adjustable of course)
Icons are managed via the IconManager
. This provides a way to creating different types of icons, such as images from the disk, bitmaps, custom geometry (SVG) and so on.
When creating an icon, you provide brushes created by the BrushManager
.
and they take brushes created by the .
Icon can be passed to context menu entries and used in toolbar buttons
Accessing underlying icon pixel data is not currently implemented but is certainly possible; SVG icons for example would have to be rendered first using
RenderTargetBitmap
.
FramePFX provides a way to dynamically add UI components, such as buttons and toggle buttons, that can execute custom user code or commands. This is to allow plugins to add UI components without having to necessarily dig into the raw UI components (like Buttons, ToggleButtons, etc.)
These are the current toolbars available and their class:
TimelineToolBarManager
: The toolbar at the bottom of the timeline. Supports west and east anchored buttonsControlSurfaceListToolBarManager
: The toolbar at the bottom of the control surface list.ViewPortToolBarManager
: The toolbar just below the view port. Supports west, center and east anchoring
This documentation is still being updated, there's a few things missing.