Convert Apache Flex views in Moonshine IDE to Feathers UI - Moonshine-IDE/Moonshine-IDE GitHub Wiki
This document is intended for contributors to Moonshine IDE. It provides an overview for the process of converting views in Moonshine IDE from Apache Flex to Feathers UI. The new Feathers UI views are written in Haxe instead of MXML/ActionScript, and they're linked to the main MoonshineDESKTOPevolved application project from a compiled .swc file.
- [DONE] Release a build of Moonshine IDE that contains a mix of Apache Flex views and Feathers UI views, still running on Adobe AIR.
- All views are converted to Haxe and Feathers UI. "Business logic" is still mostly ActionScript. Still runs on Adobe AIR.
- All ActionScript code is rewritten in Haxe. Still runs on Adobe AIR.
- Fully converted to OpenFL, meaning that it can run on other targets besides Adobe AIR, including compiled as a native C++ app.
- Views should have all of the same capabilities in Feathers UI that they had in Flex.
- Views should generally be skinned the same in Feathers UI as they were in Flex.
- Views should be de-coupled from global state. Each view should communicate with its associated plugin only, and the plugin can handle communicating with the rest of Moonshine.
- Write code understanding that we're eventually aiming to run it on targets other than Adobe AIR.
All views converted to Haxe and Feathers UI have been moved to the MoonshineGUICore project. This project is built as a .swc file, so that the classes may be used in ActionScript during the migration process. See MoonshineGUICore/README.md for details about how to build and use this project.
The LocationsView
class displays a list of source code locations, including the file path, the line number, and the column/character number. It is displayed when the Go To Definition (or Go To Type Definition) command returns multiple locations for the same definition.
|------------------------------------------|
| Go To Location [x] |
|------------------------------------------|
| SomeType (12, 1) - path/to/SomeType.h |
| SomeType (15, 7) - path/to/SomeType.cpp |
| |
| |
|------------------------------------------|
| [Open Location] |
|------------------------------------------|
In Moonshine 2.6, LocationsView is written in MXML and ActionScript as an Apache Flex component. Source code: LocationsView.mxml.
Starting in Moonshine 3.0, LocationsView is rewritten in Haxe as a Feathers UI component. Source code: LocationsView.hx.
The code snippets copied below will come from the Moonshine 3.0 version. Refer to the link above to see the snippets in the context of the full class.
Feathers UI currently doesn't have a way to represent UI using a markup language, such as how Flex has MXML. With that in mind, the first thing that you'll notice is that the component is purely written in Haxe. It's not quite as easy to see the hierarchy of the component at a glance, like with an XML-based format, but the Feathers UI component architecture is pretty easy to understand.
The top-level Haxe package we'll be using for rewritten components is moonshine
. Previously, it was actionScripts
. So actionScripts.plugin.locations.view
becomes moonshine.plugin.locations.view
.
package moonshine.plugin.locations.view;
LocationsView
is still a subclass of ResizableTitleWindow
. For a floating dialog box, this component is ideal because it has a title bar (which supports dragging the window with the mouse), a resizable bottom right corner, and a close button. All of these capabilities are configurable.
class LocationsView extends ResizableTitleWindow {
While ResizableTitleWindow
is ideal for floating dialogs, other components converted from Flex to Feathers UI may require different superclasses.
-
LayoutGroup
is commonly used as a simple container that supports layouts. - If you need layouts with scrolling,
ScrollContainer
may be useful. -
Panel
is also commonly used, and it's likeScrollContainer
with a header and footer. - For really low-level components,
FeathersControl
may be used, but it requires a bit more manual work to get right.
Inside the LocationsView
constructor, we call MoonshineTheme.initializeTheme();
before we call super()
. This is a bit unusual for a custom Feathers UI component that is written for a specific app (it's more common for a library of Feathers UI components, though). Normally, the app's theme would be initialized when the app starts, and then, all components would automatically use the theme. Since we have a mix of Apache Flex and Feathers UI in Moonshine right now, it's easier to keep the majority of Feathers UI code on the Feathers UI side. In the future, we'll be able to switch to the more typical scenario, where the app initializes the theme, and we can remove this line from all components.
public function new() {
MoonshineTheme.initializeTheme();
super();
After super()
, we configure a few basic properties. This would also be an acceptable place to add event listeners, for events like Event.ADDED_TO_STAGE
that might be needed for the lifetime of the component.
After the constructor, we define member variables and properties (you can put these before the constructor instead, if you prefer).
private var _locations:ArrayCollection<Location> = new ArrayCollection();
@:flash.property
public var locations(get, set):ArrayCollection<Location>;
private function get_locations():ArrayCollection<Location> {
return this._locations;
}
private function set_locations(value:ArrayCollection<Location>):ArrayCollection<Location> {
if (this._locations == value) {
return this._locations;
}
this._locations = value;
this._selectedLocation = null;
this.setInvalid(InvalidationFlag.DATA);
return this._locations;
}
For public properties that need to be get or set with ActionScript code in the view's plugin class, we must add @:flash.property
metadata. Normally, the Haxe compiler generates getters and setters as simple methods, so (in ActionScript) you'd normally need to call view.get_locations()
to get the value and view.set_locations(value)
to set the value. That's obviously not a normal ActionScript coding style, but if you're not mixing ActionScript and Haxe together, you'd never notice that it was compiled this way. Since we're mixing ActionScript and Haxe in Moonshine, it matters more. Luckily, the Haxe compiler has the @:flash.property
escape hatch to generate real ActionScript getters and setters instead.
(The @:flash.property
metadata is no longer required. It will be added automatically by a Haxe macro when compiling the .swc file)
The locations
property above is typed as ArrayCollection<Location>
. This is a Feathers UI collection, and it is not the same as Flex's ArrayCollection
. The two classes are very similar, though. When we make changes to the plugin class later, we'll need to make sure that we're passing data of the correct type to the Feathers UI component. To be clear, Feathers UI does not understand Flex collections, and there are currently no plans for creating a "wrapper" for them. Eventually, once we've converted everything to Haxe, Moonshine won't use any Flex collections at all.
We can create children and do any final initialization in the initialize()
method. This method is called one time only.
override private function initialize():Void {
Most of the XML hierarchy of a Flex MXML component would be re-created here, including children and the contents of <fx:Declarations>
. Generally, everything except bindings and <fx:Script>
content.
Here, we pass a VerticalLayout
to the layout
property inherited from ResizableTitleWindow
.
var viewLayout = new VerticalLayout();
viewLayout.horizontalAlign = JUSTIFY;
viewLayout.paddingTop = 10.0;
viewLayout.paddingRight = 10.0;
viewLayout.paddingBottom = 10.0;
viewLayout.paddingLeft = 10.0;
viewLayout.gap = 10.0;
this.layout = viewLayout;
Next, we create a Label
, set its text, and add it as a child.
var resultsFieldLabel = new Label();
resultsFieldLabel.text = "Matching locations:";
this.addChild(resultsFieldLabel);
If a child had an id
in MXML, you should probably create a member variable on your Haxe component class to store the value so that you can access it in event listeners or other methods.
private var resultsListView:ListView;
this.resultsListView = new ListView();
Use of this
is optional in Haxe, by the way, just like it is in ActionScript.
Make sure that you always remember to call super.initialize()
. Otherwise, some features implemented by the superclass may not behave correctly.
The update()
method is usually used to pass data to sub-components. This method will probably be called multiple times during the Feathers UI component's lifecycle. Typically, it's called when properties change, whether it's one that we write directly, like the locations
property above, but also things like width
and height
that are defined for all components.
override private function update():Void {
In the set_locations()
method above, we called setInvalid(DATA)
. In update()
, we check if this flag has been set to determine if there's new data to pass to the ListView
.
var dataInvalid = this.isInvalid(InvalidationFlag.DATA);
if (dataInvalid) {
this.resultsListView.dataProvider = this._locations;
}
This is basically how we pass data to children, to replace bindings in MXML.
The InvalidationFlag
enum defines a number of possible flags that custom Feathers UI components may use. These flags are meant for internal use in compoennts, so they don't necessarily have strict meanings that will be exactly the same for every component. Use what seems closest in meaning to your custom component's property, or use the CUSTOM()
option to create your own, if you prefer.
Always call super.update()
, or some features implemented by the superclass may not behave correctly.
After update()
, you can add additional methods and event listeners. Basically, anything that you might put into an MXML <fx:Script>
element.
Each Moonshine view is generally associated with a plugin class. The plugin is basically a place where communications between the view and the rest of Moonshine are handled. Ideally, the view won't know anything about Moonshine's global state, and it will mostly deal with value objects received from the plugin, while dispatching custom events for any changes that are required.
For reference, here's the source code for LocationsPlugin
, in both Moonshine 2.6 and 3.0.
In Moonshine 2.6, LocationsPlugin
is written in ActionScript and integrates with the Apache Flex component. Source code: LocationsPlugin.as.
In Moonshine 3.0, LocationsPlugin
is still written in ActionScript, but it uses FeathersUIWrapper
to add a Feathers UI component to the display list. Source code: LocationsPlugin.as.
In LocationsPlugin
from Moonshine 2.6, there was a variable named locationsView
that stored an instance of the Flex LocationsView
. In Moonshine 3.0, we've kept that variable to store the Feathers UI LocationsView
instance, but we've also added another variable to hold a FeathersUIWrapper
instance.
private var locationsView:LocationsView;
private var locationsViewWrapper:FeathersUIWrapper;
FeathersUIWrapper
is a special Apache Flex component that holds a Feathers UI component. To Flex, FeathersUIWrapper
is just a regular component, but internally, FeathersUIWrapper
knows how to bridge the various differences in things like measurement and resizing, focus management, and validation.
Generally, you'll create an instance of your Feathers UI component, and then immediately pass it to the constructor of FeathersUIWrapper
, like LocationsPlugin
does in its constructor.
locationsView = new LocationsView();
locationsViewWrapper = new FeathersUIWrapper(locationsView);
When you need to add the Feathers UI component to the Flex display list, you add its associated FeathersUIWrapper
instead. In the case of LocationsView
, we're adding it to Flex's PopUpManager
with PopUpManager.addPopUp()
.
var parentApp:Object = UIComponent(model.activeEditor).parentApplication;
PopUpManager.addPopUp(locationsViewWrapper, DisplayObject(parentApp), true);
PopUpManager.centerPopUp(locationsViewWrapper);
As mentioned above, new Moonshine views will use Feathers UI collections instead of Flex collections, so be sure to use the correct type for passing data to the view.
import feathers.data.ArrayCollection;
Some APIs may be named slightly differently between Flex's ArrayCollection
and Feathers UI's ArrayCollection
. For instance, collection.addItem(location)
becomes collection.add(location)
.
for(var i:int = 0; i < itemCount; i++)
{
var location:Location = locations[i];
collection.add(location);
}
To pass focus to a Feathers UI component, you should use the assignFocus()
method of its FeathersUIWrapper
. This method coordinates the Flex FocusManager
and the Feathers UI FocusManager
.
locationsViewWrapper.assignFocus("top");
When Moonshine displays a ResizableTitleWindow
, it typically centers the window (see the call to PopUpManager.centerPopUp()
above). It also listens for when Moonshine resizes, and then centers the window again.
locationsViewWrapper.stage.addEventListener(Event.RESIZE, locationsView_stage_resizeHandler, false, 0, true);
The listener simply calls PopUpManager.centerPopUp()
again.
protected function locationsView_stage_resizeHandler(event:Event):void
{
PopUpManager.centerPopUp(locationsViewWrapper);
}
The ReferencesView
and ReferencesPlugin
class are used with the Find All References command to display all references in the project to particular variable, method, or type. ReferencesView
is similar in many ways to LocationsView
, but there are a few differences worth highlighting.
__________ ____________
| Problems | References |
|----------| |__________________
| SomeType (12, 1) - path/to/SomeType.h |
| SomeType (15, 7) - path/to/SomeType.cpp |
| |
| |
|------------------------------------------|
ReferencesView
is not displayed as a pop-up. Instead, it's displayed in a tab at the bottom of the Moonshine window. Moonshine requires that the views in this tab navigator implement the IViewWithTitle
interface so that it knows what text to display in the tab. We can easily implement that interface in Haxe:
class ReferencesView extends LayoutGroup implements IViewWithTitle {
And the the title
getter is similar to other properties with @:flash.property
metadata in our Haxe views:
@:flash.property
public var title(get, never):String;
public function get_title():String {
return "References";
}
While that's pretty straightforward, things get tricky when we need to pass the ReferencesView
to the tab navigator. The tab navigator can't display the ReferencesView
directly. It needs a FeathersUIWrapper
instead, and FeathersUIWrapper
doesn't implement the IViewWithTitle
interface.
In that case, we can create a subclass of FeathersUIWrapper
that implements IViewWithTitle
.
class ReferencesViewWrapper extends FeathersUIWrapper implements IViewWithTitle {
public function ReferencesViewWrapper(feathersUIControl:ReferencesView)
{
super(feathersUIControl);
}
public function get title():String {
return ReferencesView(feathersUIControl).title;
}
}
The title
getter on ReferencesViewWrapper
simply calls the title
getter on ReferencesView
.
Moonshine sometimes uses Flex's className
property to differentiate between different views. Some existing code expects a className
property that returns "ReferencesView". We can easily override this property in ReferencesViewWrapper
.
override public function get className():String
{
return "ReferencesView";
}
The GoToLineView
class displays a dialog where the user may enter a line number in a text input to move the caret to that location in the editor.
|------------------------------------------|
| Go To Line [x] |
|------------------------------------------|
| { 5 } |
| Enter line number: (1 - 20) |
|------------------------------------------|
| [Go To Line] |
|------------------------------------------|
In Moonshine 2.6, GoToLineView
was created and added to the Flex PopUpManager
with a single call to PopUpManager.createPopUp()
. Adding a pop-up this way won't work with Feathers UI components because we need to create the component and pass it to a FeathersUIWrapper
first.
Here's what the old code with createPopUp()
looked like:
// old code with Flex GoToLineView
gotoLineView = PopUpManager.createPopUp(FlexGlobals.topLevelApplication as DisplayObject, GoToLineView, true) as GoToLineView;
It was rewritten to use addPopUp()
instead:
// new code with Feathers UI GoToLineView
gotoLineView = new GoToLineView();
gotoLineViewWrapper = new FeathersUIWrapper(gotoLineView);
PopUpManager.addPopUp(gotoLineViewWrapper, FlexGlobals.topLevelApplication as DisplayObject, true);
In general, most styling code should go into the MoonshineTheme
class. Try not to mix styling code directly into your view. If you need to style a particular UI component a bit differently from the default, create a variant in the theme.
In the MoonshineTheme
constructor, styling functions are registered with the theme. Below, you see a couple of styling functions for the Feathers UI Label
component.
this.styleProvider.setStyleFunction(Label, null, setLabelStyles);
this.styleProvider.setStyleFunction(Label, THEME_VARIANT_LIGHT_LABEL, setLightLabelStyles);
The first function, setLabelStyles()
sets the default styles for labels that don't have a variant. The second function, setLightLabelStyles()
, sets a style function for a label with the THEME_VARIANT_LIGHT_LABEL
variant, which is also defined as a static constant in MoonshineTheme
.
public static final THEME_VARIANT_LIGHT_LABEL:String = "moonshine-label--light";
To use this variant, you would create a Label
like this:
var label = new Label();
label.variant = MoonshineTheme.THEME_VARIANT_LIGHT_LABEL;
label.text = "This is some text";
The contents of the setLabelStyles()
and setLightLabelStyles()
functions appear below:
private function setLabelStyles(label:Label):Void {
label.textFormat = new TextFormat("DejaVuSansTF", 12, 0x292929);
label.disabledTextFormat = new TextFormat("DejaVuSansTF", 12, 0x999999);
label.embedFonts = true;
}
private function setLightLabelStyles(label:Label):Void {
label.textFormat = new TextFormat("DejaVuSansTF", 12, 0xf3f3f3);
label.disabledTextFormat = new TextFormat("DejaVuSansTF", 12, 0x555555);
label.embedFonts = true;
}
You can see that the two styles of labels have different font colors.
Various common utility classes and value objects are used throughout Moonshine. These classes have not yet been ported to Haxe, but they can be accessed by Haxe code if they are exposed as Haxe "externs" in the externs folder of the MoonshineGUICore project. If an extern class is missing, feel free to add it. Some existing extern classes may be missing properties or methods, and they may be added, if needed. Warning: As explained above, try to avoid exposing global singletons like IDEModel
and GlobalEventDispatcher
as extern classes.
Here's a simple example of how to create an extern class:
package actionScripts.valueObjects;
extern class MyExternClass {
public var someVariable:String;
@:flash.property
public var someGetter(default, never):Bool;
@:flash.property
public var someGetterAndSetter(default, default):Int;
public function someMethod(arg:Float):Void;
}
- Flex views may contain Feathers UI views, but Feathers UI views must never contain Flex views. When you start converting a component to Feathers UI, expect to convert all of its children at the same time. Start as deep as you find managable, and work your way up the display list.
- If a Flex component makes use of global state, such as the
GlobalEventDispatcher
orIDEModel
singletons, it may be easier to refactor the Flex component to move that global state into the associated plugin class before converting the component to Feathers UI. Have the Flex view dispatch events to the plugin and allow the plugin to set properties on the view. When you're ready to rewrite the view using Feathers UI, it will be easier to use the same event and property names, knowing that it is already working in Flex. - If you write Haxe code with AIR-only APIs (such as file system access or native process launching), expect to rewrite that code in the future. In some cases, Moonshine provides abstractions that will help, such as the
FileLocation
andFileWrapper
classes. These were originally written to allow the core Moonshine codebase to be used to build both a desktop app and a web app, but they also come in handy for moving from AIR to Haxe/OpenFL. There isn't a Moonshine alternative toNativeProcess
at this time, so this code will need to stick with AIR APIs for now and we'll modify it to handle multiple targets in later milestones. - If a feature is missing in Feathers UI, work with Josh Tynjala to get it added. If you're not sure whether or not a feature is available after checking the docs, ask Josh and he can guide you.
- Try to avoid committing the binary .swc file too frequently. It's often best to wait until you need others to test your changes. Multiple versions of binary files can make our Git repository very larger. In the future, once all code is written in Haxe, we won't need to build and commit this .swc file anymore.