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.

Major milestones

  • [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.

High level goals

  • 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.

MoonshineGUICore project

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.

Analysis of LocationsView

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 like ScrollContainer 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.

LocationsPlugin

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);
}

Implementing interface with FeathersUIWrapper (ReferencesView)

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";
}

Replacing PopUpManager.createPopUp() calls (GoToLineView)

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);

Styling in MoonshineTheme

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.

Externs

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;
}

Additional tips

  • 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 or IDEModel 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 and FileWrapper 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 to NativeProcess 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.

Further Reading

⚠️ **GitHub.com Fallback** ⚠️