Overview
As mentioned in the Design Philosophy, everything in carefree-drawboard
🎨 is a plugin, so it will be important to understand how plugins are built / work. In this page, we will cover the common parts that every plugin will share, and leave those specific parts to separate pages.
How to -
Find / Use Plugins?
In the future, we will implement a built-in marketplace for plugins, so that you can publish / search / download / use them easily. For now, you may need to build your own plugins from scratch. 😣
However, since plugins can be built within one single file, for now you can share your plugins by simply sharing the source code of the plugin file. 😆
And, as we introduced in the Getting Started, we have a Register Mechanism for the plugins, so you can include others' plugins without worrying too much - unless you import & register them, no side effects will be caused.
Publish Plugins?
As mentioned above, we will implement a built-in marketplace for plugins in the future, so publishing plugins will be very easy. But for now, you may need to share your plugins by:
- Simply sharing the source code of the plugin file.
- Follow the Contributing guide and submit a PR.
Build Plugins?
That's what this page is mainly about. 😎 Stay tuned!
Basic Concepts
A Plugin
in carefree-drawboard
🎨 usually consists of two parts:
- The
plugin button
, which is used to trigger the expansion of theexpand panel
. - The
expand panel
, which is used for users to interact with the plugin.
For some special plugins (e.g., Delete
, Download
, Undo
, Redo
, etc.), the 'expand panel
' will be omitted because user interaction ends with the click of the plugin button
.
So a typical workflow of a plugin will be:
- Users click the
plugin button
. - The
expand panel
is expanded. - Users interact with the
expand panel
, e.g., fill in some input fields. - Users click the
Submit
button to send the filled inputs to the backend. - The
expand panel
is collapsed, theplugin button
is showing some progress indicators during the processing (e.g., a progress bar), and the backend will be processing the user inputs. - The drawboard 🎨 will be updated once the processing is done.
To make things easier, we extracted the common parts of the plugins into the Built-in Bindings, so you only need to concern about:
- What should be the Styles of the
plugin button
and theexpand panel
? - What should be the Logics when the
Submit
button is clicked and the user inputs are sent to the backend?
After all things are done, you or other users can utilize the Register Mechanism to register the plugins into the drawboard 🎨. Before which, no side effects will be caused.
And in the following sections, we will cover these concepts in detail.
Inheritance
To utilize the common parts of the plugins, you need to inherit from one of the Built-in Bindings (e.g., IFieldsPlugin
).
from cfdraw import *
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(...)
async def process(self, data: ISocketRequest):
...
The first import line
from cfdraw import *
is a typical import line for every carefree-drawboard
🎨 app. It will import all the necessary stuffs for you to build your plugins.
Styles
In carefree-drawboard
🎨, we specify Styles in the settings
property.
from cfdraw import *
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(...)
async def process(self, data: ISocketRequest):
...
IPluginSettings
As the highlighted lines show, we should return an instance of IPluginSettings
in the settings
property.
We return a class instance instead of a dict
here because we want to utilize the auto-completion feature of IDEs.
The IPluginSettings
can be used to specify:
- The Styles of the
plugin button
and theexpand panel
. - The behaviours of the
expand panel
, this is specified in a separate class -IPluginInfo
.
Although we can also treat 'behaviours' as 'Styles', we separate them for better understanding.
Example
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(
# width of the expanded panel
w=300,
# height of the expanded panel
h=180,
# tooltip of the plugin
tooltip="Apply Gaussian Blur to the image",
# specify that this plugin should only appear when an image is selected
nodeConstraint=NodeConstraints.IMAGE,
# specify that this plugin should appear at the right-top of the selected image
pivot=PivotType.RT,
# specify that this plugin should 'follow' the selected image when it is dragged / resized
follow=True,
)
Most properties of the IPluginSettings
are pretty self-explanatory, while the nodeConstraint
, pivot
and follow
properties are a bit tricky. See Plugin Positioning for more details.
IPluginInfo
This is where you specify the behaviours of the expand panel
. In reality, we should use one of the Built-in Bindings (e.g., IFieldsPluginInfo
) instead of using the IPluginInfo
directly.
We introduce Built-in Bindings because every plugin has some common behaviours & some specific behaviours, so we extract the common behaviours into the IPluginInfo
, while leave the specific behaviours to the Built-in Bindings.
Example
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(
...,
pluginInfo=IFieldsPluginInfo(
...,
# specify that the plugin should stay expanded even after the users click the 'Submit' button
closeOnSubmit=False,
# specify that the plugin should show a toast message after the users click the 'Submit' button
toastOnSubmit=True,
# specify the toast message to be shown after the users click the 'Submit' button
toastMessageOnSubmit="Gaussian Blur applied!",
),
)
Logics
In carefree-drawboard
🎨, we specify logics in the process
method.
from cfdraw import *
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(...)
async def process(self, data: ISocketRequest):
...
As you can see, the only argument of the process
method is data
, which is an instance of ISocketRequest
. Generally speaking, it contains the following information:
- User data (e.g.,
userId
). - Data of the user inputs.
- Data of the selecting
Node
(s) on the drawboard 🎨.
So it is enough for most if not every scenario!
Accessibility
However, providing all necessary data is not the end of the story, we also need to make sure that we can utilize the data in a convenient way. For example, for any plugin that requires processing an image, we should be able to get a PIL.Image
instance as easy as possible.
Therefore, carefree-drawboard
🎨 provides a set of Built-in Methods to help you out. A typical example is the load_image
method, which can help you download an image from a url in an async
way:
from cfdraw import *
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(...)
async def process(self, data: ISocketRequest):
url = data.nodeData.src
image = await self.load_image(url) # type: PIL.Image
...
Register Mechanism
In carefree-drawboard
🎨, every plugin needs to be registered to actually work. Let's say you defined a plugin in the my_plugin.py
file:
from cfdraw import *
class MyPlugin(IFieldsPlugin):
...
Then in your main app file (e.g., app.py
), you can register it like this:
from cfdraw import *
from my_plugin import MyPlugin
register_plugin("my_plugin")(MyPlugin)
app = App()
If there's another plugin called MyPlugin
as well in another file (e.g., my_plugin2.py
), and you want to register it as well, you can:
from cfdraw import *
from my_plugin import MyPlugin as MyPlugin1
from my_plugin2 import MyPlugin as MyPlugin2
# Notice that you need to keep the 'name' you passed to the
# `register_plugin` function unique across all registered plugins!
register_plugin("my_plugin1")(MyPlugin1)
register_plugin("my_plugin2")(MyPlugin2)
app = App()
carefree-drawboard
🎨 introduced this register mechanism because it can make the whole system:
- More decoupled. You can now define plugins freely without worrying about any side effects.
- More extensible. If you want to use plugins implemented by others, you can simply import & register them.
Reference
Built-in Bindings
In carefree-drawboard
🎨, a Built-in Binding often refers to two things:
- A class for your plugin to inherit from.
- A class which inherits from
IPluginInfo
for you to specify thepluginInfo
property ofIPluginSettings
.
For example, the most commonly used Built-in Binding is IFieldsPlugin
, and you should use it like this:
from cfdraw import *
class Plugin(IFieldsPlugin):
@property
def settings(self) -> IPluginSettings:
return IPluginSettings(
...,
pluginInfo=IFieldsPluginInfo(...),
)
async def process(self, data: ISocketRequest):
...
And here's a table of all the supported Built-in Bindings:
Plugin Base Class | IFieldsInfo inheritor |
---|---|
IFieldsPlugin | IFieldsPluginInfo |
IChatPlugin | IChatPluginInfo |
IPluginGroup | IPluginGroupInfo |
You may ask: why do we need a Base Class for the plugin?
In fact, Base Class is no more than an ISocketPlugin
with a pre-defined type
property. For example, here's the complete code of IFieldsPlugin
:
class IFieldsPlugin(ISocketPlugin):
@property
def type(self) -> PluginType:
return PluginType.FIELDS
So with a Base Class, you don't need to speciy the type
property anymore. Now the question becomes: why do we need a type
property? The answer is: we need it in case Middleware wants to subscribe for only a / some specific type(s) of plugins. For example, the TimerMiddleware
will only subscribe for plugins that are inherited from IFieldsPlugin
:
class TimerMiddleware(IMiddleware):
...
@property
def subscriptions(self) -> List[PluginType]:
return [PluginType.FIELDS]
class IFieldsPlugin(ISocketPlugin):
@property
def type(self) -> PluginType:
return PluginType.FIELDS
Plugin Positioning
The positioning of the plugins in carefree-drawboard
🎨 is relatively simple. We only need to keep three things in mind:
- Should the plugin always be displayed, or should the plugin be displayed if and only if certain
Node
(s) are selected? - Should the plugin 'follow' the selected
Node
(s), or should it stick at the edge of the drawboard 🎨? - Which
Pivot
should we place our plugin?
Let's show some examples below to demonstrate the ideas!
dict(
nodeConstraint=NodeConstraints.IMAGE,
follow=True,
pivot=PivotType.RT,
)
- The plugin will be displayed if and only if the
ImageNode
is selected. - The plugin will follow the
rt
(right top) corner of (the bounding box of) theImageNode
.
dict(
nodeConstraint=NodeConstraints.NONE,
follow=False,
pivot=PivotType.RT,
)
- The plugin will always be displayed.
- The plugin will be placed at the
rt
(right top) corner of the entire drawboard 🎨.
dict(
nodeConstraintRules=NodeConstraintRules(
exactly=[NodeConstraints.IMAGE, NodeConstraints.PATH]
)
follow=True,
pivot=PivotType.RT,
)
- The plugin will be displayed if and only if (exactly) an
ImageNode
& aPathNode
are selected. - The plugin will follow the
rt
(right top) corner of (the bounding box of) the selectedNode
s.